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

迭代器

迭代器是依附于迭代协议的对象——基本意味它有一个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。

(0)

相关推荐

  • Python 详解基本语法_函数_返回值

    Python 详解基本语法 概要: 函数的返回值是函数重要的组成部分.函数的根本在于实现程序的部分功能,所以很多时候我们需要将函数执行后的结果返回给程序再由程序作出进一步的操作.可以说是函数的返回值令函数与函数之间,函数与主程序之间更加紧密的联系起来. 函数的返回值 在Python的函数中都有一个返回值,默认为None.也可以使用return value语句来定义一个且只能定义一个可为任意类型的返回值.但是我们能够返回一个序列类型的对象,来实现返回多个值的效果. Example: 返回一个Lis

  • Python生成器(Generator)详解

    通过列表生成式,我们可以直接创建一个列表.但是,受到内存限制,列表容量肯定是有限的.而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了. 所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间.在Python中,这种一边循环一边计算的机制,称为生成器(Generator). 简单生成器 要创建一个generator,有很

  • 深入讲解Python中的迭代器和生成器

    在Python中,很多对象都是可以通过for语句来直接遍历的,例如list.string.dict等等,这些对象都可以被称为可迭代对象.至于说哪些对象是可以被迭代访问的,就要了解一下迭代器相关的知识了. 迭代器 迭代器对象要求支持迭代器协议的对象,在Python中,支持迭代器协议就是实现对象的__iter__()和next()方法.其中__iter__()方法返回迭代器对象本身:next()方法返回容器的下一个元素,在结尾时引发StopIteration异常. __iter__()和next()

  • Python的迭代器和生成器使用实例

    一.迭代器Iterators 迭代器仅是一容器对象,它实现了迭代器协议.它有两个基本方法: 1)next方法 返回容器的下一个元素 2)__iter__方法 返回迭代器自身 迭代器可使用内建的iter方法创建,见例子: 复制代码 代码如下: >>> i = iter('abc') >>> i.next() 'a' >>> i.next() 'b' >>> i.next() 'c' >>> i.next() Trace

  • Python使用迭代器捕获Generator返回值的方法

    本文实例讲述了Python使用迭代器捕获Generator返回值的方法.分享给大家供大家参考,具体如下: 用for循环调用generator时,发现拿不到generator的return语句的返回值.如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中: #!/usr/bin/env python # -*- coding: utf-8 -*- def fib(max): n, a, b = 0, 0, 1 while n < max:

  • 深入学习python的yield和generator

    前言 没有用过的东西,没有深刻理解的东西很难说自己会,而且被别人一问必然破绽百出.虽然之前有接触过python协程的概念,但是只是走马观花,这两天的一次交谈中,别人问到了协程,顿时语塞,死活想不起来曾经看过的东西,之后突然想到了yield,但为时已晚,只能说概念不清,所以本篇先缕缕python的生成器和yield关键字. 什么是生成器 1.生成器是一个特殊的程序,可以被用作控制循环的迭代行为 2.生成器类似于返回值为数组的一个函数,这个函数可以接收参数,可以被调用,但是,不同于一般的函数会一次性

  • shell脚本中执行python脚本并接收其返回值的例子

    1.在shell脚本执行python脚本时,需要通过python脚本的返回值来判断后面程序要执行的命令 例:有两个py程序  hello.py 复制代码 代码如下: def main():     print "Hello" if __name__=='__main__':     main() world.py def main():     print "Hello" if __name__=='__main__':     main() shell 脚本 te

  • python生成器generator用法实例分析

    本文实例讲述了python生成器generator用法.分享给大家供大家参考.具体如下: 使用yield,可以让函数生成一个结果序列,而不仅仅是一个值 例如: def countdown(n): print "counting down" while n>0: yield n #生成一个n值 n -=1 >>> c = countdown(5) >>> c.next() counting down 5 >>> c.next()

  • 举例讲解Python中的迭代器、生成器与列表解析用法

    迭代器:初探 上一章曾经提到过,其实for循环是可用于任何可迭代的对象上的.实际上,对Python中所有会从左至右扫描对象的迭代工具而言都是如此,这些迭代工具包括了for循环.列表解析.in成员关系测试以及map内置函数等. "可迭代对象"的概念在Python中是相当新颖的,基本这就是序列观念的通用化:如果对象时实际保存的序列,或者可以再迭代工具环境中一次产生一个结果的对象,那就看做是可迭代的. >>文件迭代器 作为内置数据类型的文件也是可迭代的,它有一个名为__next_

  • python的迭代器与生成器实例详解

    本文以实例详解了python的迭代器与生成器,具体如下所示: 1. 迭代器概述:   迭代器是访问集合元素的一种方式.迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器只能往前不会后退,不过这也没什么,因为人们很少在迭代途中往后退.   1.1 使用迭代器的优点   对于原生支持随机访问的数据结构(如tuple.list),迭代器和经典for循环的索引访问相比并无优势,反而丢失了索引值(可以使用内建函数enumerate()找回这个索引值).但对于无法随机访问的数据结构(比

  • python迭代器与生成器详解

    例子 老规矩,先上一个代码: def add(s, x): return s + x def gen(): for i in range(4): yield i base = gen() for n in [1, 10]: base = (add(i, n) for i in base) print list(base) 这个东西输出可以脑补一下, 结果是[20,21,22,23], 而不是[10, 11, 12, 13]. 当时纠结了半天,一直没搞懂,后来齐老师稍微指点了一下, 突然想明白了-

  • Python中用函数作为返回值和实现闭包的教程

    函数作为返回值 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回. 我们来实现一个可变参数的求和.通常情况下,求和的函数是这样定义的: def calc_sum(*args): ax = 0 for n in args: ax = ax + n return ax 但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数! def lazy_sum(*args): def sum(): ax = 0 for n in args:

  • Python函数返回值实例分析

    本文实例讲述了Python的函数返回值用法.分享给大家供大家参考.具体分析如下: 最近学一些Python的基本用法,看到定义函数,发现似乎只能返回一个返回值,想想matlab里返回多个返回值多方便啊,网上查了查,看到有返回多个值的方法.   python 函数返回值有两种形式: 1 返回一个值. 2 返回多个值.   现看看返回一个值的吧. def firstvalue(a,b): c = a + b return c print firstvalue(1,2) #结果:3 再看看返回多个值的:

随机推荐