>>> from env_helper import info; info()
页面更新时间: 2023-12-27 10:45:13
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

6.12. 熟悉Python的生成器

生成器.顾名思义,就是按一定的算法生成一个序列,比如产生自然数序列、斐波那 契数列等。之前讲迭代器的时候,就讲过一个生成波那契数列的例子。那么迭代器也是生成 器?其实不然。迭代器虽然在某些场景表现得像生成器,但它绝非牛成器;反而是生成器实 现了迭代器协议的,可以在一定程度上看作迭代器。再把话题转回迭代器样式的斐波那契数 列实现,熟悉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__',
 '__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_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;不然大家总是会往函数方面去想的a

当第一次调用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")
TypeError('spam')

除了能yield表达式的“返回值”之外,也可以让它抛出异常,这就是throw()方法的能 力。在本例中,yield value表达式抛出一个TypeError异常,然后被内层的except语句捕获, 并赋值给value,因此整个代码的执行流并没有离开whUe循环块,所以进人了下一次循环。 当再次执行yield value时,异常对象 (也就是value的值)被返回到此次throw()调用中。对 于常规业务逻辑的代码来说,处理异常的情况不会像这个例子中那样,而是对特定的异常有 很好的处理(比如将异常信息写人日志后优雅地返回从而实现从外部影响生成器内部的控制流。

>>> generator.close()
Don't forget to clean up when 'close()'' is called

当调用close()方法时,yield表达式就抛出GeneratorExit异常,生成器对象会自行处理 这个异常c当调用close()之后,再次调用next()、send()会使生成器对象抛出Stoplteration 异常,换言之,这个生成器对象已经不可再用。最后值得一提的是,当生成器对象被GC回 收时,会自动调用close()。

除了简化前文中使用迭代器协议生成斐波那契数列的代码之外,生成器还有两个很棒的 用处,其中之一是实现with语句的上下文管理器协议,利用的是调用生成器函数时函数体并 不执行,当第一次调用next()方法时才开始执行,并执行到yield表达式后中止,直到下一 次调用next()方法这个特性;其二是实现协程,利用的是上文所述的send()、throw()、close() 等特性在此,继续讲述第一个应用,而第二个应用留待下一小节讲述。

首先,需要我们回过头来重温一下上下文管理器协议,其实就是要求类实现 __enter__()方和__exit__()方法。比如以下file对象就实现了这个协议:

>>> with open('tmp.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也可以用以“池” 模式中对资源的管理和囬收,具体的实现留给大家去思考。