返回介绍

建议66:熟悉 Python 的生成器

发布于 2024-01-30 22:19:09 字数 4147 浏览 0 评论 0 收藏 0

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

看,代码行数是不是减少了许多?这就是yield关键字的魅力。不过要掌握这个关键字可不容易,首先来看看fib()函数返回的是什么。

>>> f = fib(10)
>>> type(f)
<type 'generator'>
>>> dir(f)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', 
  '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', 
  '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 
  '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 
  'next', '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, e:
...       value = e
...  finally:
...    print "Don't forget to clean up when 'close()' is called."
...
>>> generator = echo(1)
>>> print generator.next()
Execution starts when 'next()' is called for the first time.
1

至此,可以看到每一个生成器函数调用之后,它的函数体并不执行,而是到第一次调用next()的时候才开始执行。这一点未免让新手颇为费解,但目前来看除了硬记住这一点外并无它法。要从根源上解决问题的话,可能需要约定生成器函数使用另外一个关键字,比如使用generator而不是def,不然大家总是会往函数方面去想的。

当第一次调用next()方法时,生成器函数开始执行,执行到yield表达式为止。如例子中的value=(yield value)语句中,只是执行了yield value这个表达式,而赋值操作并未执行。记住这一点很重要,只有记住了这一点,才能理解后续的内容,如send()方法。

>>> print generator.next()
None

这个也让人有点困惑,按代码应当是返回1的,怎么返回None了呢?这时候需要注意的是代码中的value=(yield value),yield是一个表达式,所以它可以作为一个表达式的右值。当第二次调用next()时,yield表达式的值赋值给了value,而yield表达式的默认“返回值”就是None,所以后续value的值就是None。现在再用自然语言来描述一次第二次调用next()的过程,首先是value=(yield value)语句中的赋值操作得到了执行,即value被赋值为None,然后是while条件判断,再次进入循环体,执行value=(yield value)语句,此时value的值为None,yield出来的也是None,那么再次调用next()时返回None就顺理成章了,因为next()的返回值就是yield表达式的右值。

>>> print generator.send(2)
2

直率地说,send()方法很绕,这不是一个好名字。其实send()是全功能版本的next(),或者说next()是send()的“快捷方式”,相当于send(None)。还记得yield表达式有一个“返回值”吗?send()方法的作用就是控制这个返回值,使得yield表达式的“返回值”是它的实参。

>>> 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()会使生成器对象抛出StopIteration异常,换言之,这个生成器对象已经不可再用。最后值得一提的是,当生成器对象被GC回收时,会自动调用close()。

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

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文