生成器 Generator 的定义
生成器(Generator)是一种特殊的函数,可以用于迭代地生成一系列值,而不需要一次性生成所有值并将它们存储在内存中。生成器在需要时逐个生成值,并在生成值后暂停执行,保留函数的状态,以便下次调用时能够从停止的地方继续执行。
生成器函数使用 yield 语句来定义,而不是常规函数中的 return 语句。当生成器函数被调用时,它返回一个生成器对象,而不是立即执行函数体。每次调用生成器对象的 next() 方法时,生成器函数将从上次执行停止的位置继续执行,直到遇到下一个 yield 语句。生成器通过生成一个值并将其返回给调用者来实现迭代。
什么是Python的生成器 Generator
上面的定义 过于官方, 理解有点困难。
通俗地讲,
- 生成器就是一种特别的迭代器(iterator)。
- 生成器的大部分使用方法是跟迭代器一样的
- 生成器代码更简洁, 而且具有临时修改当前元素的功能
一个简单的生成器例子
直接看代码:
python
def gen(num):
while num > 0:
yield num # return num to next() and pause the function
num -= 1
return # raise StopIteration
g = gen(5) # g is a generator object
print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3
for i in g:
print(i) # 2 1
简单代码解释
1 到 5 行是 定义1个 生成器函数 gen()
第8行是基于生成器函数 定义了 了1个生成器对象 g
之后下面的代码就是可以把 g 当成iterator 来使用了, 遍历它的元素。
初步理解生成器函数
首先, 生成器 与 迭代器 1个明显的区别是, 迭代器是用类来定义的, 迭代器器的对象必须实现__next__ 函数
而且生成器是用 函数来定义的, 而里面的关键字就是 yield
一旦python 中某个函数 写入了 yield 这个关键字, 则代表python 不会把这个函数当作普通函数, 而且1个种特别的函数 - 生成器函数
生成器的函数的
yield 可以理解为return 返回1个值
而生成器最后的return 可不是普通 函数 return的意思, 实际是代表 raise StopIteration, 没有后面的元素的了的意思。
初步理解生成器对象
我们再看看上面例子的第8行
python
def gen(num):
while num > 0:
yield num # return num to next() and pause the function
num -= 1
return # raise StopIteration
g = gen(5)
g = gen(5) 如果按照一般函数的思维 g 肯定是 gen() 的返回值, 由于gen()里的return 后面是吉的, 所以 g = None 吗?
实际上, 由于gen() 里面包含关键字yield, 所以它是1个生成器函数, 所以执行这g = gen(5) 时, 实际上gen()里面的代码一句都还没开始执行。
而且上面说了, gen() 里面的return 并不是真正的return , 就算我们写成 return 100, g 也不会是100!
所以我们可以认为 g = gen(5) 简单看为定义了1个g 生成器对象。
生成器函数里的代码什么时候被执行
实际上, 生成器的代码只会在 生成器对象的 next 函数调用时 才开始执行
例如上面例子中的
print(next(g))
这时, gen(5) 开始被调用
当执行到yield 时到底发生了什么
继续上面, 当gen(5) 被第一次调用时, 自然从
python
while num > 0 # num = 5
这句代码开始执行, 由于num 初始值为5, 这是由 g = gen(5) 定义时决定的。
当执行到下一句代码
python
yield = num
这时 gen() 函数会把num return 出去
所以 print(next(g)) 就会在console 里输出 num的值 5了, 这就是为何上面说的 yield 具有普通函数 return的功能
yield 还会中断生成器函数的执行, 并保存为下次执行的开始位置
先讲中断执行, 因为yield 具有return 的效果, 所以当执行到yield 函数就退出了
print(next(g)) 会输出数字5
很容易理解
关键是保存当前的位置,并不是很容易理解
我们继续看回例子:
python
def gen(num):
while num > 0:
yield num # return num to next() and pause the function
num -= 1
return # raise StopIteration
g = gen(5) # g is a generator object
print(next(g)) # 5
print(next(g)) # 4
这时, 第10行已经执行完成, 开始执行第11行的
print(next(g))
而这时, gen() 函数会从第5行开始执行(因为上次执行执行到 第4行
python
yield num
时退出了
这时执行第5行
python
num -= 1
num 变成了4
由于这时代码在gen()里的while里
所以 代码会返回到第2行
py
while num >0
这时num的值为4, 还是大于0, 再执行第3行
py
yield num
这时再次中断 ,返回num
所以这时 print(next(g))就会在console 输出数字4了
关键, 当gen生成器函数 被再次调用, 它实际上是应该在上一次调用yield 的下一行开始执行
yield 应该在gen()的循环体(例如while) 内使用
很明显, 想保存 生成器的函数执行位置 , yield 应该在循环体内调用, 否则就直接 到了return # raise StopInteration , 失去了迭代元素的功能
例如:
上面例子可以由while 循环改成for 循环
python
def gen(num): # from num to 1, not includes 0
for i in range(num, 0, -1):
yield i
return # raise StopIteration
g = gen(5) # g is a generator object
print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3
for i in g:
print(i) # 2 1
生成器 与 迭代器 保存当前元素的方法 的区别
由于生成器和迭代器的使用方法几乎相同。
他们都可以在遍历的过程中保存当前的元素
但是迭代器是保存在迭代器对象里的1个属性当中的。
例如:
python
class LinkListIterator:
def __init__(self, _first_node) -> None:
self._current_node = _first_node
def __iter__(self):
return self
def __next__(self):
if not self._current_node:
raise StopIteration
current = self._current_node
self._current_node = current.next
return current.value
中的self._current_node
而且生成器是利用yield 关键字保存在python 的框架中的.
生成器 支持用send() 方法临时修改当前保存的元素
我们看1个例子:
python
def gen(num):
while num > 0:
tmp = yield num # return num to next() and pause the function
if tmp:
num = tmp
num -= 1
return # raise StopIteration
g = gen(5)
print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3
print(g.send(None)) # 2 # equal to next(g)
print(g.send(10)) # 9
print("=============================================")
for i in g:
print(i) # 8 7 6 5 4 3 2 1
上面例子中, 我们使用了1个变量tmp 去保存 yield num的输出
然后我们就可以用g.send() 去修改这个这个tmp
然后我们用
python
if tmp:
num = tmp
去修改num 的值, 就是实现了 临时修改 生成器步骤的功能
看例子 在15 行, g.send(None) 效果就是send 1个None 给tmp, 并不会修改num 的值, 所以这里的g.send(None) 等同于 next(g)
关键来了, 在第16行, 我们调用g.send(10) 为何输出的是9 而不是10?
当我们调用g.send(10) 时, gen() 函数会在上次调用yield 的下一行执行, 就是从第4行 if tmp:
但是第7行num-=1 会被执行, 所以下次yield输出就是9了
如果向避免这种gap可以把 num -= 1 写进else 的block里
如:
python
def gen(num):
while num > 0:
tmp = yield num # return num to next() and pause the function
if tmp:
num = tmp
else:
num -= 1
return # raise StopIteration
g = gen(5)
print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3
print(g.send(None)) # 2 # equal to next(g)
print(g.send(10)) # 10
print("=============================================")
for i in g:
print(i) # 9 8 7 6 5 4 3 2 1
总结
好了到这里生成器的基本特性已经介绍完
我会在之后的文章里介绍生成器的一些常用使用场景