生成器的工作原理
只要Python函数的主体中有yield关键字,该函数就是生成器函数。调用生成器函数,返回一个生成器对象。也就是说,生成器函数是生成器工厂。
下面以一个简单的函数说明生成器的行为:
def gen123():
yield 1
yield 2
yield 3
print(gen123) # <function gen123 at 0x000002A486B4A200>
print(gen123()) # <generator object gen123 at 0x000002A486AF7270>
for i in gen123():
print(i) # 1,2,3
g = gen123()
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
print(next(g)) # StopIteration
可以看出,在函数主体中我们使用了3个yield,输出gen123是函数对象,但是gen123()是个生成器对象。生成器对象实现了Iterator接口,因此生成器对象可以迭代。我们把gen123()赋值给g,因为g是迭代器,所以调用next(g)会获取yield产出下一项,直到所有项产出完以后,抛出StopIteration异常。
生成器函数创建一个生成器对象,包装生成器函数的主体。把生成器对象传递给next()时,生成器函数提前执行函数主体中的下一个yield语句, 返回产出的值,并在函数主体的当前位置暂停。最终,函数的主体返回时,Python创建的外层生成器对象抛出StopIteration异常。
惰性生成器
我们看下面的代码:
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return f"Sentence({reprlib.repr(self.text)})"
def __iter__(self):
for word in self.words:
yield word
上面代码Sentence.__iter__是一个生成器函数,虽然如此,但是还不够惰性,因为__init__已经及早的构建了数据列表且绑定到self.words属性上,这样已经处理了整个文本,列表使用的内存和文本本身一样多,如果我们只想迭代前面几个单词,那么大多数工作都是在白费力气。
如今,人们认为惰性是好的特质,至少在编程语言和API中是如此。惰性实现是指尽可能延后生成值。这样能节省内存,或许还可以避免浪费CPU循环。那么上面代码可以实现惰性操作吗?那必须滴。
finditer()是findall()的惰性版本,返回的不是一个列表,而是一个生成器,按需产出re.MatchObject实例,如果文本够长,finditer()能节省大量内存,因为它只在需要时从文本读取单词。
惰性生成器表达式
生成器已经极大简化代码了,不过使用生成器表达式能让代码更精简。
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
s = Sentence('你好 谢谢 再见')
for i in s:
print(i)
# 输出
# 你好
# 谢谢
# 再见
和上面代码唯一区别就是,__iter__方法这里使用的不是生成器函数(没有yield),而是使用生成器表达式构建生成器。然后将其返回。不过,最终效果都是一样的:__iter__方法的调用得到一个生成器对象。
生成器表达式是语法糖(( i for i in xxx)),完全可以用生成器函数替代,不过有时候使用生成器表达式更便利。
何时使用生成器表达式
生成器表达式是创建生成器的简洁句法,无须先定义后调用。不过生成器函数更为灵活,可以使用多个语句实现复杂逻辑,甚至可以作为协程使用。
根据我的经验:如果生成器表达式要分成多行编写,那我更倾向于定义生成器函数,增强代码的可读性。
对比迭代器和生成器
迭代器:
泛指实现了__next__方法的对象。迭代器用于生成供客户代码使用的数据,即客户代码通过for循环或其他迭代方式,或者直接在迭代器上调用next(it)驱动迭代器。不过显示调用next()不常见。实际上,Python中使用的迭代器多数是生成器。
生成器:
由Python编译器构造的迭代器。为了构建生成器,我们不使用__next__方法,而是使用yield关键字得到生成器函数。。生成器表达式是构建生成器对象的 另一种方式。生成器对象提供了 __next__ ⽅法,因此⽣成器对 象是迭代器。