- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议66:熟悉 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
看,代码行数是不是减少了许多?这就是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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论