Python中的迭代器与生成器高级用法解析


Posted in Python onJune 28, 2016

迭代器

迭代器是依附于迭代协议的对象——基本意味它有一个next方法(method),当调用时,返回序列中的下一个项目。当无项目可返回时,引发(raise)StopIteration异常。

迭代对象允许一次循环。它保留单次迭代的状态(位置),或从另一个角度讲,每次循环序列都需要一个迭代对象。这意味我们可以同时迭代同一个序列不只一次。将迭代逻辑和序列分离使我们有更多的迭代方式。

调用一个容器(container)的__iter__方法创建迭代对象是掌握迭代器最直接的方式。iter函数为我们节约一些按键。

>>> nums = [1,2,3]   # note that ... varies: these are different objects
>>> iter(nums)              
<listiterator object at ...>
>>> nums.__iter__()           
<listiterator object at ...>
>>> nums.__reversed__()         
<listreverseiterator object at ...>

>>> it = iter(nums)
>>> next(it)      # next(obj) simply calls obj.next()
1
>>> it.next()
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

当在循环中使用时,StopIteration被接受并停止循环。但通过显式引发(invocation),我们看到一旦迭代器元素被耗尽,存取它将引发异常。

使用for...in循环也使用__iter__方法。这允许我们透明地开始对一个序列迭代。但是如果我们已经有一个迭代器,我们想在for循环中能同样地使用它们。为了实现这点,迭代器除了next还有一个方法__iter__来返回迭代器自身(self)。

Python中对迭代器的支持无处不在:标准库中的所有序列和无序容器都支持。这个概念也被拓展到其它东西:例如file对象支持行的迭代。

>>> f = open('/etc/fstab')
>>> f is f.__iter__()
True

file自身就是迭代器,它的__iter__方法并不创建一个单独的对象:仅仅单线程的顺序读取被允许。

生成表达式
第二种创建迭代对象的方式是通过 生成表达式(generator expression) ,列表推导(list comprehension)的基础。为了增加清晰度,生成表达式总是封装在括号或表达式中。如果使用圆括号,则创建了一个生成迭代器(generator iterator)。如果是方括号,这一过程被‘短路'我们获得一个列表list。

>>> (i for i in nums)          
<generator object <genexpr> at 0x...>
>>> [i for i in nums]
[1, 2, 3]
>>> list(i for i in nums)
[1, 2, 3]

在Python 2.7和 3.x中列表表达式语法被扩展到 字典和集合表达式。一个集合set当生成表达式是被大括号封装时被创建。一个字典dict在表达式包含key:value形式的键值对时被创建:

>>> {i for i in range(3)}  
set([0, 1, 2])
>>> {i:i**2 for i in range(3)}  
{0: 0, 1: 1, 2: 4}

如果您不幸身陷古老的Python版本中,这个语法有点糟:

>>> set(i for i in 'abc')
set(['a', 'c', 'b'])
>>> dict((i, ord(i)) for i in 'abc')
{'a': 97, 'c': 99, 'b': 98}

生成表达式相当简单,不用多说。只有一个陷阱值得提及:在版本小于3的Python中索引变量(i)会泄漏。

生成器

生成器是产生一列结果而不是单一值的函数。

第三种创建迭代对象的方式是调用生成器函数。一个 生成器(generator) 是包含关键字yield的函数。值得注意,仅仅是这个关键字的出现完全改变了函数的本质:yield语句不必引发(invoke),甚至不必可接触。但让函数变成了生成器。当一个函数被调用时,其中的指令被执行。而当一个生成器被调用时,执行在其中第一条指令之前停止。生成器的调用创建依附于迭代协议的生成器对象。就像常规函数一样,允许并发和递归调用。
当next被调用时,函数执行到第一个yield。每次遇到yield语句获得一个作为next返回的值,在yield语句执行后,函数的执行又被停止。

>>> def f():
...  yield 1
...  yield 2
>>> f()                  
<generator object f at 0x...>
>>> gen = f()
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

让我们遍历单个生成器函数调用的整个历程。

>>> def f():
...  print("-- start --")
...  yield 3
...  print("-- middle --")
...  yield 4
...  print("-- finished --")
>>> gen = f()
>>> next(gen)
-- start --
3
>>> next(gen)
-- middle --
4
>>> next(gen)              
-- finished --
Traceback (most recent call last):
 ...
StopIteration

相比常规函数中执行f()立即让print执行,gen不执行任何函数体中语句就被赋值。只有当gen.next()被next调用,直到第一个yield部分的语句才被执行。第二个语句打印-- middle --并在遇到第二个yield时停止执行。第三个next打印-- finished --并且到函数末尾,因为没有yield,引发了异常。

当函数yield之后控制返回给调用者后发生了什么?每个生成器的状态被存储在生成器对象中。从这点看生成器函数,好像它是运行在单独的线程,但这仅仅是假象:执行是严格单线程的,但解释器保留和存储在下一个值请求之间的状态。

为何生成器有用?正如关于迭代器这部分强调的,生成器函数只是创建迭代对象的又一种方式。一切能被yield语句完成的东西也能被next方法完成。然而,使用函数让解释器魔力般地创建迭代器有优势。一个函数可以比需要next和__iter__方法的类定义短很多。更重要的是,相比不得不对迭代对象在连续next调用之间传递的实例(instance)属性来说,生成器的作者能更简单的理解局限在局部变量中的语句。

还有问题是为何迭代器有用?当一个迭代器用来驱动循环,循环变得简单。迭代器代码初始化状态,决定是否循环结束,并且找到下一个被提取到不同地方的值。这凸显了循环体——最值得关注的部分。除此之外,可以在其它地方重用迭代器代码。

双向通信
每个yield语句将一个值传递给调用者。这就是为何PEP 255引入生成器(在Python2.2中实现)。但是相反方向的通信也很有用。一个明显的方式是一些外部(extern)语句,或者全局变量或共享可变对象。通过将先前无聊的yield语句变成表达式,直接通信因PEP 342成为现实(在2.5中实现)。当生成器在yield语句之后恢复执行时,调用者可以对生成器对象调用一个方法,或者传递一个值 给 生成器,然后通过yield语句返回,或者通过一个不同的方法向生成器注入异常。

第一个新方法是send(value),类似于next(),但是将value传递进作为yield表达式值的生成器中。事实上,g.next()和g.send(None)是等效的。

第二个新方法是throw(type, value=None, traceback=None),等效于在yield语句处

raise type, value, traceback

不像raise(从执行点立即引发异常),throw()首先恢复生成器,然后仅仅引发异常。选用单次throw就是因为它意味着把异常放到其它位置,并且在其它语言中与异常有关。

当生成器中的异常被引发时发生什么?它可以或者显式引发,当执行某些语句时可以通过throw()方法注入到yield语句中。任一情况中,异常都以标准方式传播:它可以被except和finally捕获,或者造成生成器的中止并传递给调用者。

因完整性缘故,值得提及生成器迭代器也有close()方法,该方法被用来让本可以提供更多值的生成器立即中止。它用生成器的__del__方法销毁保留生成器状态的对象。

让我们定义一个只打印出通过send和throw方法所传递东西的生成器。

>>> import itertools
>>> def g():
...   print '--start--'
...   for i in itertools.count():
...     print '--yielding %i--' % i
...     try:
...       ans = yield i
...     except GeneratorExit:
...       print '--closing--'
...       raise
...     except Exception as e:
...       print '--yield raised %r--' % e
...     else:
...       print '--yield returned %s--' % ans

>>> it = g()
>>> next(it)
--start--
--yielding 0--
0
>>> it.send(11)
--yield returned 11--
--yielding 1--
1
>>> it.throw(IndexError)
--yield raised IndexError()--
--yielding 2--
2
>>> it.close()
--closing--

注意: next还是__next__?

在Python 2.x中,接受下一个值的迭代器方法是next,它通过全局函数next显式调用,意即它应该调用__next__。就像全局函数iter调用__iter__。这种不一致在Python 3.x中被修复,it.next变成了it.__next__。对于其它生成器方法——send和throw情况更加复杂,因为它们不被解释器隐式调用。然而,有建议语法扩展让continue带一个将被传递给循环迭代器中send的参数。如果这个扩展被接受,可能gen.send会变成gen.__send__。最后一个生成器方法close显然被不正确的命名了,因为它已经被隐式调用。

链式生成器
注意: 这是PEP 380的预览(还未被实现,但已经被Python3.3接受)

比如说我们正写一个生成器,我们想要yield一个第二个生成器——一个子生成器(subgenerator)——生成的数。如果仅考虑产生(yield)的值,通过循环可以不费力的完成:

subgen = some_other_generator()
for v in subgen:
  yield v

然而,如果子生成器需要调用send()、throw()和close()和调用者适当交互的情况下,事情就复杂了。yield语句不得不通过类似于前一章节部分定义的try...except...finally结构来保证“调试”生成器函数。这种代码在PEP 380中提供,现在足够拿出将在Python 3.3中引入的新语法了:

yield from some_other_generator()

像上面的显式循环调用一样,重复从some_other_generator中产生值直到没有值可以产生,但是仍然向子生成器转发send、throw和close。

Python 相关文章推荐
Python 正则表达式(转义问题)
Dec 15 Python
python通过ssh-powershell监控windows的方法
Jun 02 Python
python开发中range()函数用法实例分析
Nov 12 Python
Python编写简单的HTML页面合并脚本
Jul 11 Python
Python脚本实时处理log文件的方法
Nov 21 Python
教你使用python画一朵花送女朋友
Mar 29 Python
pycharm编写spark程序,导入pyspark包的3中实现方法
Aug 02 Python
django 控制页面跳转的例子
Aug 06 Python
pytorch实现线性拟合方式
Jan 15 Python
python实现用户名密码校验
Mar 18 Python
Python创建简单的神经网络实例讲解
Jan 04 Python
pycharm 复制代码出现空格的解决方式
Jan 15 Python
Python设计足球联赛赛程表程序的思路与简单实现示例
Jun 28 #Python
详解Python中heapq模块的用法
Jun 28 #Python
Python中operator模块的操作符使用示例总结
Jun 28 #Python
基础的十进制按位运算总结与在Python中的计算示例
Jun 28 #Python
Python中的with语句与上下文管理器学习总结
Jun 28 #Python
深入解析Python中的上下文管理器
Jun 28 #Python
详解Python中contextlib上下文管理模块的用法
Jun 28 #Python
You might like
解决MySQL中文输出变成问号的问题
2008/06/05 PHP
PHP连接局域网MYSQL数据库的简单实例
2013/08/26 PHP
php表单文件iframe异步上传实例讲解
2017/07/26 PHP
javascript 屏蔽鼠标键盘的几段代码
2008/01/02 Javascript
刷新页面实现方式总结(HTML,ASP,JS)
2008/11/13 Javascript
动态添加js事件实现代码
2009/03/12 Javascript
javascript 异常处理使用总结
2009/06/21 Javascript
jquery实现的超出屏幕时把固定层变为定位层的代码
2010/02/23 Javascript
关于javascript function对象那些迷惑分析
2011/10/24 Javascript
5个最佳的Javascript日期处理类库分享
2012/04/15 Javascript
jquery实现的带缩略图的焦点图片切换(自动播放/响应鼠标动作)
2013/01/23 Javascript
jquery实现倒计时代码分享
2014/06/13 Javascript
javascript模拟post提交隐藏地址栏的参数
2014/09/03 Javascript
JavaScript常用的弹出广告及背投广告实现方法
2015/02/06 Javascript
使用console进行性能测试
2015/04/27 Javascript
javascript的几种写法总结
2016/09/30 Javascript
angularjs实现下拉列表的选中事件示例
2017/03/03 Javascript
angular中不同的组件间传值与通信的方法
2017/11/04 Javascript
详解Vue路由钩子及应用场景(小结)
2017/11/07 Javascript
create-react-app修改为多页面支持的方法
2018/05/17 Javascript
vue-cli2与vue-cli3在一台电脑共存的实现方法
2019/09/25 Javascript
jquery+css3实现的经典弹出层效果示例
2020/05/16 jQuery
详解vue3.0 diff算法的使用(超详细)
2020/07/01 Javascript
vue3.0 上手体验
2020/09/21 Javascript
[00:56]2014DOTA2国际邀请赛 DK、iG 赛前探访
2014/07/10 DOTA
python threading模块操作多线程介绍
2015/04/08 Python
python下paramiko模块实现ssh连接登录Linux服务器
2015/06/03 Python
Python中的单行、多行、中文注释方法
2018/07/19 Python
pycharm打开命令行或Terminal的方法
2019/01/16 Python
python读取ini配置文件过程示范
2019/12/23 Python
计算Python Numpy向量之间的欧氏距离实例
2020/05/22 Python
使用OpenCV获取图像某点的颜色值,并设置某点的颜色
2020/06/02 Python
法学专业个人求职信
2013/09/26 职场文书
小学数学教研活动总结
2014/07/01 职场文书
2014年标准化工作总结
2014/12/17 职场文书
winserver2019安装软件一直卡在应用程序正在为首次使用做准备
2022/06/10 Servers