一篇文章看懂 Python iterable, iterator 和 generator - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
onlyice
V2EX    Python

一篇文章看懂 Python iterable, iterator 和 generator

  •  5
     
  •   onlyice 2019-04-27 22:36:29 +08:00 3320 次点击
    这是一个创建于 2391 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Python 中的 iterable, iterator 以及 generator,一直是非常亲密但是难以区分的概念。nvie 有一个很好的 帖子 阐述了它们之间的关系,但是内容偏向于概括和总结,对于新手来说仍然难以理解。Fluent Python 的第 14 章也有非常好的演绎,但是我认为它对「为什么要有这种语言特性」缺乏阐释。我试图从演变的角度,总结这些概念的来源和演化,以得到一个符合逻辑和容易理解的版本。

    Simple Loop

    几乎每一个 Python 入门教程,都会用类似下面的代码来讲述最简单的 for 循环:

    >>> l = [2, 1, 3] >>> for i in l: ... print(i) 2 1 3 

    在 Python 中,执行 l[i] 实际上是调用了 l__getitem__ 函数list 类型中会实现了这个函数,用来返回某个 index 下的元素。而早期 Python 的实现上利用了这个操作。上面的 for 循环实际上是从 l[0] 开始取元素,等价于这段代码:

    i = 0 while True: try: print(l[i]) # 亦可是 print(l.__getitem__(i)) i += 1 except IndexError: break 

    Python 内置的大多数容器类型(list, tuple, set, dict)都支持 __getitem__,因此它们都可以用在 for .. in 循环中。如果你有个自定义类型也想用在循环中,你需要在类里面实现一个 __getitem__ 函数,Python 就会识别到并使用它。Fluent Python 一书提供了一个例子

    Lazy Evaluation

    在上面的代码例子中,l 的值是全部被加载到内存中,再在循环中被一个一个取出来的。设想这样一个场景,你要从数据库中查询出一千万条数据做处理,

    • 如果全部加载到内存,可能会将内存撑满
    • 在处理第一条数据前,需要等待大量时间从数据库中取出这些数据
    • 一些特殊的场景下,你可能并不需要对全部的数据做处理,比如处理到第五百万条数据时即可以结束

    前辈们提出了惰性求值( Lazy Evaluation )来解决这个问题。有些地方也叫它「延迟加载」「懒加载」等。它的基本理念是「按需加载」,在上面的例子中,可以将取数据过程变成一页页取,比如先取 100 条数据进行处理,处理完后再取下一个 100 条,直至全部取完。

    The Iterator Protocol

    Python 为了在语言层面支持 lazy evaluation,给出了 iterator 协议。如果一个类实现了 __next__ 函数,并且:

    • 每次调用该函数,都可以返回一个新的数据
    • 没有新的数据时,调用它抛出 StopIteration 异常(当然如果序列是无限长,那么可以不抛)

    那么这个类即支持 iterator 协议。于是「按需加载」,即可以通过每次 __next__ 被调用时去实现。Python 的内置函数 next(iterator) 实际上是调用 iterator.__next__。下面的例子给出一个 iterator 的实现,用来按需地计算出下一个斐波那契数:

    >>> class FibonacciIterator: ... def __init__(self, maximum): ... # 为了简单,将初始值设为 1, 2 而不是 0, 1 ... self.a, self.b = 1, 2 ... self.maximum = maximum ... def __next__(self): ... fib = self.a ... if fib > self.maximum: ... raise StopIteration ... self.a, self.b = self.b, self.a + self.b ... return fib >>> f = FibonacciIterator(5) >>> next(f) 1 >>> next(f) 2 >>> next(f) 3 >>> next(f) 5 >>> next(f) # StopIteration occured 

    Enhanced iterable

    上文中的 FibonacciIterator 已经实现了按需加载,那可以直接将它用在 for 循环中吗?试试:

    >>> for i in FibonacciIterator(5): ... print(i) ... Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'FibonacciIterator' object is not iterable 

    可以看到有 is not iterable 的报错。按上一节的描述,早期的 Python 仅在一个类有 __getitem__ 函数时 Python 才将它当成 iterable,同时为了配合新的 iterator 的机制,Python 在 2.2 版本中将 __iter__ 协议加入了进来:

    1. 一个类如果实现了 __iter__ 并返回一个 iterator,那么它是 iterable 的
    2. 如果没有实现 __iter__,但是有 __getitem__,那么它仍然是 iterable 的

    那么对于一个 iterator,如果你想能在 for 循环中使用,只需要实现一个 __iter__ 函数返回自己就可以了:

    >>> class FibonacciIterator: ... # 其他函数实现省略,见上文 ... def __iter__(self): ... return self >>> for i in FibonacciIterator(5): ... print(i) 1 2 3 5 

    Generator Function

    上面虽然花了挺大篇幅讲述 iterator 的机制,但是事实上 Python 中以 __next__ 方式来实现 iterator 的并不多。Python 在 2.2 版本中支持了 iterator,但是也同时给出了另外一种更灵活方便,也更重要的机制 generator。

    识别 generator 的标志在 yield 关键字。上文的斐波那契数列,用 generator 来实现是:

    >>> def fib(maximum): ... a, b = 1, 2 ... while a <= maximum: ... yield a ... a, b = b, a+b ... >>> for i in fib(5): ... print(i) 1 2 3 5 
    <>上面的 fib() 虽然也是用 def 定义,但是它的函数体中有 yield 关键字,因此它不是个普通函数,而是个 generator function。它返回的是一个 generator object,即 fib(5) 处。generator 是一种在语言层面被支持的 iterator,它的规则是:

    • next() 调用一个 generator 时,Python 会执行函数体到下一个 yield 语句,并将 yield 后的值作用 next() 的返回值;然后该函数的执行暂停,直至下一次 next() 调用时,继续执行到下一个 yield
    • 当整个函数体被执行完毕时,抛出 StopIteration 异常

    这套规则清晰直观,可以将它套用在上面代码中验证一下。yield 及 generator 是非常重要的机制,不仅仅在于它比 iterator 更简单直观,而在于它同时引入了一种控制语言运行的机制。对于普通函数,一旦执行则必须从函数头执行到函数尾,之后才把控制权交给调用方;但是对于 generator function,你可以只执行一小段代码,即把控制权交回调用方(yield 时)。这种机制也对后面提出 coroutine 及 asyncio 中起到了重要的作用。

    Generator Expression

    试试运行下面的代码:

    >>> sum([i**2 for i in range(11)]) 385 >>> sum((i**2 for i in range(11))) 385 >>> sum(i**2 for i in range(11)) 385 

    后两种写法,跟第一种有什么区别呢?后两种即是 generator expression,是一种方便生成 generator 的语法糖,形式上是用括号包裹的 list comprehension。背后的理念大概是这样:list comprehension 是用来生成元素的,generator 也是用来生成元素的,那为什么不提供一种类似 list comprehension 语法的 expression 来表示 generator 呢?它跟下面的代码是等价的:

    def gen(): for i in range(11): yield i**2 sum(gen()) 

    至于第三种写法为啥不用括号包起来,是 Python 为了可读性故意设计的,如果作为唯一的函数参数使用,则可以省略。

    总结

    定义上:

    • 实现了 __iter____getitem__,并满足一定规则的类型是 iterable 的,它的实例是个 iterable
    • 实现了 __next__ 并满足一定规则的类型,它是一种 iterator,它的实例是个 iterator
    • 在函数体中使用了 yield 的函数是 generator function ;使用了括号包裹的类 list comprehension 是 generator expression。它们都会产生 generator

    语言机制上:

    • for .. in 循环所消费的对象,需要是个 iterable

    它们之间的关联:

    • 容器类型(list, dict, etc.)大部分是 iterable ;dict 有多个不同函数生成不同用途的 iterator
    • iterator 大部分情况下是个 iterable
    • generator 是个 iterator,同时是个 iterable
    12 条回复    2020-05-06 16:42:15 +08:00
    piglei
        1
    piglei  
       2019-04-27 23:04:07 +08:00   1
    很棒的介绍。生成器这东西设计的还是有点,基本不用了解底层迭代协议,就能完成很多牛逼的事情。
    im67
        2
    im67  
       2019-04-28 00:28:44 +08:00   1
    改日细看
    xiexingjia
        3
    xiexingjia  
       2019-04-28 00:33:29 +08:00   2
    内无公众号宣传,可安心阅读
    arzterk
        4
    arzterk  
       2019-04-28 09:30:57 +08:00   1
    zqguo
        5
    zqguo  
       2019-04-28 09:35:55 +08:00   1
    写的不错,已收藏。
    baday
        6
    baday  
       2019-04-28 15:33:19 +08:00
    所有迭代器应该都是可迭代对象, 只实现了__next__的并不是一个迭代器 FibonacciIterator。
    onlyice
        7
    onlyice  
    OP
       2019-04-28 16:37:21 +08:00
    @baday #6 从我看到的信息源中,并没有要求 Iterator 必须是 iterable 的。你能给出官方文档描述这块吗?
    onlyice
        8
    onlyice  
    OP
       2019-04-28 16:39:52 +08:00
    @baday #6 我找到了,谢谢指正。
    baday
        9
    baday  
       2019-04-28 16:41:07 +08:00   1
    @onlyice 你可以实例化之后,isinstance 判断一下是不是 collection.Iterator 的实例
    tengyoubiao
        10
    tengyoubiao  
       2020-02-06 20:34:30 +08:00
    好文挖个坟不算过分。
    讲的很细致了,通读下来大概有了了解,日后再读应该还会有收获。
    purplemystic
        11
    purplemystic  
       2020-04-29 08:49:32 +08:00
    purplemystic
        12
    purplemystic  
       2020-05-06 16:42:15 +08:00
    Generator 继承自 Iterator
    Iterator 继承自 Iterable
    他们仨,是祖孙三代.
    https://blog.caoyu.info/iterable-iterator-in-python.html
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1026 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 22:53 PVG 06:53 LAX 14:53 JFK 17:53
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86