- 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章 性能剖析与优化
建议85:使用生成器提高效率
斐波那契数列相信大家都不陌生,这是常见的编程题目,也是很多书籍中喜欢引用的例子。我们这里也不免落于俗套,就以这个例子开场吧。斐波那契是一个简单的递归数列,数列满足这样的规律:除了前两个数,任何其他数都可以由其前面两个数相加得到,即f(N)=f(N-1)+f(N-2),n>2。Python中有多种方法实现这个数列,我们来看其中的一种。
>>> def fab(n): ... i,a,b = 0,0,1 ... foblist = [] ... while i < n: ... foblist.append(b) ... a,b = b,a+b # 不借助中间变量交换两个变量的方法 ... i = i+1 ... return foblist ... >>> print fab(4) [1, 1, 2, 3]
想一想,上面的例子有没有更好的实现方法呢?显然有!在介绍具体实现之前我们先来了解生成器的有关知识。
生成器的语法在Python2.2中就引入了,但实际应用过程中还是有人不会选择使用它,特别是有过其他语言基础的,主要是思维上难以转换过来。生成器的概念其实非常简单,如果一个函数体中包含有yield语句,则称为生成器(generator),它是一种特殊的迭代器(iterator),也可以称为可迭代对象(iterable)。可迭代对象、迭代器、生成器这三者之间的关系可以简单地表示成如图8-4所示的形式。
图8-4 可迭代对象、迭代器、生成器三者之间的关系
对生成器的调用会返回一个迭代器,使用next()方法可以获取下一个元素或者抛出StopIteration异常。
>>> def mygen(x): ... for i in range(x): ... yield i ... >>> d = mygen(1) >>> d # 生成器对象,拥有iter() 和next() 方法 <generator object mygen at 0x005162B0> >>> d.next() 0 >>> d.next() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>>
实际上当需要在循环过程中依次处理一个序列中的元素的时候,就应该考虑生成器。当然,要深入理解生成器,必须透彻理解yield语句。yield语句与return语句相似,当解释器执行遇到yield的时候,函数会自动返回yield语句之后的表达式的值。不过与return不同的是,yield语句在返回的同时会保存所有的局部变量以及现场信息,以便在迭代器调用next()或者send()方法的时候还原,而不是直接交给垃圾回收器(return()方法返回后这些信息会被垃圾回收器处理)。这样就能够保证对生成器的每一次迭代都会返回一个元素,而不是一次性在内存中生成所有的元素。自Python2.5开始,yield语句变为表达式,可以直接将其值赋给其他变量,如x=(yield y)。结合一个例子来看yield语句在生成器函数调用的时候执行状态,下面的函数代表数列1,-3,5,-7,9,……。
>>> def series(): ... print "begin:" ... m=1.0; n = 1 ... print "while begin" ① ... while(1): ② ... print "yield a data" ... yield m/n ③ ... m = m+2 ④ ... n = n* -1 ... print "end" ... >>>
上面的代码用状态机可以表示为如图8-5所示的形式。状态机中状态1表示从函数定义到标注1处的所有语句的集合,状态2表示标注2的语句,状态3表示标注2到3之间的所有语句的集合,状态4表示从标注4后到结束的语句。
图8-5 示例程序的状态机形式的表示
运行上面的程序你会惊讶地发现,即使while中的条件永远为真,代码也不会陷入无限循环的状态,而是每调用一次next()方法产生一个数,这样生成器的优势就体现出来了。
>>> d = series() >>> d.next() # 第一次调用next 执行到状态2 ,循环条件为1 ,转到状态3 ,遇到yield 语句返回对 # 应的值并保留现场 begin: while begin yield a data 1.0 >>> d.next() # 之后每次调用d.next() 从状态3 开始转向状态4 ,也就是yield 语句之后的第一句 # 语句开始执行,4 直接转到2 ,循环条件永远为真,从而再次转到状态3 end yield a data -3.0
生成器的优点总体来说有如下几条:
生成器提供了一种更为便利的产生迭代器的方式,用户一般不需要自己实现__iter__和next方法,它默认返回一个迭代器。
代码更为简洁、优雅。
充分利用了延迟评估(Lazy evaluation)的特性,仅在需要的时候才产生对应的元素,而不是一次生成所有的元素,从而节省了内存空间,提高了效率,理论上无限循环成为可能而不会导致MemoryError,这在大数据处理的情形下尤为重要。
使得协同程序更为容易实现。协同程序是有多个进入点,可以挂起恢复的函数,这基本就是yield的工作方式。Python2.5之后生成器的功能更加完善,加入了send()、close()和throw()方法。其中send()不仅可以传递值给yield语句,而且能够恢复生成器,因此生成器能大大简化协同程序的实现。
现在我们回过头来看看本节开头的例子,使用生成器来实现是不是更为简洁呢?
>>> def fib(n): ... a = b =1 ... for i in range(n): ... yield a ... a,b = b,a+b ...
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论