生成器,顾名思义,就是按一定的算法生成一个序列,比如产生自然数序列、斐波那契数列等。
之前讲迭代器的时候,就讲过一个生成波那契数列的例子。那么迭代器也是生成器?其实不然。
迭代器虽然在某些场景表现得像生成器,但它绝非生成器;反而是生成器实现了迭代器协议的,
可以在一定程度上看作迭代器。再把话题转回迭代器样式的斐波那契数列实现,
熟悉Python的人会觉得其实不简洁,因为还有 yield
表达式可以简化它。
生成器
大概是因为生成器的用处巨大,所以Python中专门有一个关键字来实现它,就是 yield
。
甚至生成器的定义也与这个关键字有关:如果一个函数,使用了 yield
语句,那么它就是一个生成器函数。
当调用生成器函数时,它返回一个迭代器,不过这个迭代器是以生成器对象 的形式出现的。
所以现在来重写一下之前的斐波那契数列实现。
def fib (n):
a, b = 1, 1
while a < n:
yield a
a,b = b, a + b
for i, f in enumerate (fib(10)):
print(f)
1 1 2 3 5 8
看,代码行数是不是减少了许多?这就是 yield
关键字的魅力。
不过要掌握这个关键字可不容易,首先来看看 fib()
函数返回的是什么。
f = fib(10)
type(f)
generator
dir(f)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_suspended', 'gi_yieldfrom', 'send', 'throw']
可以看到它返回的是一个 generator
类型的对象,而这个对象带有__iter__()
和 next()
方法,
可见的确是一个迭代器。但那些 next()
、 send()
、 throw()
、 close()
等方法是怎么回事?
要理解这些方法,需要重温一下手册中的例子。
def echo(value=None):
print("Execution starts when 'next()' is called for the first time")
try:
while True:
try:
value = (yield value)
except Exception as e:
value = e
finally:
print("Don't forget to clean up when 'close()'' is called")
generator = echo(1)
print(next(generator))
Execution starts when 'next()' is called for the first time 1
至此,可以看到每一个生成器函数调用之后,它的函数体并不执行,
而是到第一次调用 next()
的时候才开始执行。这一点未免让新手颇为费解,
但目前来看除了硬记住这一点外并无它法。要从根源上解决问题的话,
可能需要约定生成器函数使用另外一个关键字,比如使用 generator
而不是 def
;
不然大家总是会往函数方面去想的。
当第一次调用 next()
方法时,生成器函数开始执行,执行到 yield
表达式为止。
如例子中的 value=(yield value)
语句中,只是执行了 yield value
这个表达式,
而赋值操作并未执行。 记住这一点很重要,只有记住了这一点,才能理解后续的内容,如 send()
方法。
print(next(generator))
None
这个也让人有点困惑,按代码应当是返回1的,怎么返回 None
了呢?
这时候需要注意的是代码中的 value=(yield value)
, yield
是一个表达式,
所以它可以作为一个表达式的右值。 当第二次调用 next()
时, yield
表达式的值赋值给了 value
,
而 yield
表达式的默认“返回值" 就是 None
,所以后续 value
的值就是 None
。
现在再用自然语言来描述一次第二次调用 next()
的过程,
首先是 value=(yield value)
语句中的赋值操作得到了执行,即 value
被赋值为 None
,
然后是 while
条件判断,再次进入循环体,执行 vahie=(yidd value)
语句,此时value的值为 None
,
yield
出来的也是 None
,那么再次调用 next()
时返回 None
就顺理成章了,
因为 next()
的返回值就是 yield
表达式的右值。
print(generator.send(2))
2
直率地说, send()
方法很绕,这不是一个好名字。其实 SeiKl()
是全功能版本的 next()
,
或者说 next()
是 send()
的“快捷方式”,相当于 send(None)
。
还记得 yietd
表达式有一个“返回值”吗? send()
方法的作用就是控制这个返回值,
使得 yield
表达式的“返回值“是它的实参。
generator.throw(TypeError,"spam")
/tmp/ipykernel_1331/2957320472.py:1: DeprecationWarning: the (type, exc, tb) signature of throw() is deprecated, use the single-arg signature instead. generator.throw(TypeError,"spam")
TypeError('spam')
除了能 yield
表达式的“返回值”之外,也可以让它抛出异常,这就是 throw()
方法的能力。
在本例中, yield value
表达式抛出一个 TypeError
异常,然后被内层的 except
语句捕获,
并赋值给 value
,因此整个代码的执行流并没有离开 while
循环块,所以进入了下一次循环。
当再次执行 yield value
时,异常对象 (也就是 value
的值)被返回到此次 throw()
调用中。
对于常规业务逻辑的代码来说,处理异常的情况不会像这个例子中那样,
而是对特定的异常有很好的处理(比如将异常信息写入日志后优雅地返回从而实现从外部影响生成器内部的控制流。
generator.close()
Don't forget to clean up when 'close()'' is called
当调用 close()
方法时, yield
表达式就抛出 GeneratorExit
异常,
生成器对象会自行处理这个异常。当调用 close()
之后,再次调用 next()
、
send()
会使生成器对象抛出 Stoplteration
异常,换言之,这个生成器对象已经不可再用。
最后值得一提的是,当生成器对象被GC回收时,会自动调用 close()
。
上下文管理器
除了简化前文中使用迭代器协议生成斐波那契数列的代码之外,生成器还有两个很棒的用处,
其中之一是实现 with
语句的上下文管理器协议,利用的是调用生成器函数时函数体并不执行,
当第一次调用 next()
方法时才开始执行,并执行到 yield
表达式后中止,
直到下一次调用 next()
方法这个特性;其二是实现协程,利用的是上文所述的 send()
、
throw()
、 close()
等特性在此,继续讲述第一个应用,而第二个应用留待下一小节讲述。
首先,需要回过头来重温一下上下文管理器协议,
其实就是要求类实现 __enter__()
方和__exit__()
方法。
比如以下 file
对象就实现了这个协议:
with open('xxtmp.txt','w') as f:
f.write('hello, context manager')
但是生成器对象并没有这两个方法,
所以 contextlib
提供了 contextmanager
函数来适配这两种协议。
from contextlib import contextmanager
@contextmanager
def tag(name):
print("<%s>" %name)
yield
print("</%s>" %name)
with tag("h1"):
print("foo")
<h1> foo </h1>
这是来自Python文档的例子,当进入 with
块的时候, tag()
函数块的第一行执行,
并在执行到第二行的时候中止;离开 with
块的时候,执行 print “foo”
,
完成后执行 yield
后 面的语句,也就是 tag()
函数块的第三行,然后整个函数执行完毕。
通过 contextmanager
对 next()
, throw()
、 close()
的封装,
yield
大大简化了上下文管理器的编程复杂度,对提高代码 可维护性有着极大的意义。
除了上面这个例子之外, yield
和 contextmangcr
也可以用以“池” 模式中对资源的管理,
具体的实现留给大家去思考。