内容简介:如果做严格区分,迭代器(iterator)和生成器(generator)是两个概念。迭代器是用于从集合中挨个获取元素,要求数据已存在;而生成器则是“凭空”生成元素,最典型的就是斐波那契数列。但是在Python中,大多数时候迭代器和生成器被视作同一概念。从Python2.2开始,可以使用本篇将有如下内容:当Python解释器需要迭代对象
如果做严格区分,迭代器(iterator)和生成器(generator)是两个概念。迭代器是用于从集合中挨个获取元素,要求数据已存在;而生成器则是“凭空”生成元素,最典型的就是斐波那契数列。但是在 Python 中,大多数时候迭代器和生成器被视作同一概念。从Python2.2开始,可以使用 yield
关键字构建生成器,其作用和迭代器一样。在Python3中,生成器有了更广泛的用途,比如 range()
函数返回的就是一个类似生成器的对象,而在以前,它返回的是完整的列表。
本篇将有如下内容:
iter() yield from
2. 可迭代对象与迭代器
2.1 iter()函数
当Python解释器需要迭代对象 x
时,会自动调用 iter(x)
。内置的 iter()
函数的运行过程如下:
- 检查对象是否实现了
__iter__
方法,如果实现了就调用它来获取一个迭代器; - 如果没有实现
__iter__
方法,但实现了__getitem__
方法,Python会创建一个迭代器,尝试从索引0开始获取元素; - 如果上述操作都失败了,Python抛出
TypeError
异常,通常会提示“T object is not iterable”
,其中T
是目标对象所属的类。
而从上述解释可以看出,任何Python序列都可迭代的原因是,它们都实现了 __getitem__
方法。但 iter()
函数之所以要检查 __getitem__
方法,除了能让更多对象可迭代之外,其实还为了向下兼容。至于 iter()
以后还检不检查 __getitem__
方法就很难说了(不过目测未来很长一段时间内应该不会改变这种策略),而标准的序列类型都实现了 __iter__
方法,所以,如果自定义类要实现可迭代,请实现 __iter__
方法。
由此,我们还可得出 可迭代的对象 的定义:
实现了 __iter__
方法,能获取迭代器;或者实现了 __getitem__
方法,能从零开始索引的对象都是可迭代的对象。
补充 :
-
从Python3.4开始,检查对象
x
能否迭代 ,最准确的方法是:调用iter(x)
,如果不可迭代,再处理TypeError
异常。这比使用isinstance(x, abc.Iterable)
更准确,因为abc.Iterable
不会考虑__getitem__
方法。 -
iter()
函数还有一个鲜为人知的用法,即:传入两个参数,使用常规的函数或任何可调用对象创建迭代器。此时,第一个参数必须是可调用对象,第二个参数是“哨兵”。当可调用对象返回的值与“哨兵”相等时,抛弃该值,结束迭代并抛出StopIteration
异常。这种用法的一个实际情况就是读取文件,当读取到空行或文件末尾时,停止读取:# 代码2.1 with open("test.txt") as fp: for line in iter(fp.readline, "\n"): process_line(line) 复制代码
2.2 迭代器
首先需要明确可迭代对象和迭代器之间的关系: Python从可迭代对象中获取迭代器 。当对象实现了 __iter__
方法时,Python从它获取迭代器;当对象只实现了 __getitem__
方法时,Python为这个对象创建迭代器。所以, Python在迭代时始终用的是迭代器!
标准迭代器的UML继承关系图如下:
从上图以及之前的描述,我们可以总结出以下几点:
- 具体的可迭代对象 的
__iter__
方法应该返回一个 具体的迭代器 ; - 具体的迭代器 必须实现
__next__
和__iter__
方法。__iter__
方法返回迭代器本身(return self
);真正的迭代操作由__next__
完成,当没有可迭代元素时,它还要抛出StopIteration
异常; - 由于迭代器也是从
Iterable
派生出来的,所以, 迭代器是可迭代对象!
从上述内容可以猜出,应该有一个 next()
函数与 iter()
函数配对。没错,对可迭代对象的具体迭代操作就是由 next()
函数完成。以下是两个迭代过程:
# 代码2.2 s = "ABC" # 方法1,Python会隐式创建迭代器,并捕获StopIteration异常 for char in s: print(char) # 方法2,显式创建迭代器并显式迭代,此时需要手动捕获StopIteration异常 it = iter(s) while True: try: print(next(it)) except StopIteration: del it break 复制代码
如果我们要实现 具体的迭代器 ,并不一定需要从 collections.abc.Iterator
继承,只需要实现 __next__
和 __iter__
方法即可。在Python的 Lib/types.py
源文件有如下注释:
# Iterators in Python aren't a matter of type but of protocol. A large # and changing number of builtin types implement *some* flavor of # iterator. Don't check the type! Use hasattr to check for both # "__iter__" and "__next__" attributes instead. 复制代码
所以,这里可以给迭代器下个定义: 实现了 __next__
和 __iter__
方法的对象就是迭代器 。如果再去查看 abc.Iterator
的源码,可以发现如下代码:
# 代码2.3 class Iterator(Iterable): -- snip -- @classmethod def __subclasshook__(cls, C): # 做了更改,实际是调用 _check_methods(C, '__iter__', '__next__') if cls is Iterator: if (any("__next__" in B.__dict__ for B in C.__mro__) and any("__iter__" in B.__dict__ for B in C.__mro__)): return True # 希望大家看到NotImplemented能想到Python解释器后面会有什么操作 return NotImplemented # 如果猜不到,可以查看《Python学习之路32》 复制代码
综上, Iterator
采用的是白鹅类型技术:它实现了 __subclasshook__
方法,通过判断对象 x
是否实现了 __next__
和 __iter__
来判断 x
是否是迭代器。所以, 判断对象 x
是否为迭代器的最好方法是调用 isinstance(x, abc.Iterator)
。
***友情提示:***通过迭代器不能判断是否还有剩余的元素,迭代器也不能重置。当然,你可以为迭代器添加其他方法来实现这两种功能,但并不推荐这种做法,除非这代码只有你自己欣赏。如果想要重新迭代,请再次调用 iter()
函数,并传入之前的可迭代对象,传入迭代器是没有用。
2.3 典型的迭代器
下面通过实现一个 Sentence
类和与之配对的 SentenceIterator
来演示传统迭代器的实现过程:
# 代码2.4 import re import reprlib RE_WORD = re.compile("\w+") class Sentence: def __init__(self, text): self.text = text self.words = RE_WORD.findall(text) def __iter__(self): return SentenceIterator(self.words) class SentenceIterator: def __init__(self, words): self.words = words self.index = 0 # 保存索引 def __next__(self): try: word = self.words[self.index] except IndexError: # 超出索引范围时抛出异常 raise StopIteration() self.index += 1 # 递增索引 return word def __iter__(self): return self # 返回迭代器本身 复制代码
这里需要指出一个典型的 错误 思想:把 Sentence
变为迭代器。迭代器是可迭代对象,但可迭代对象 不能 是迭代器!请 不要 在可迭代对象的 __iter__
中返回可迭代对象自身,也 不要 为可迭代对象添加 __next__
方法!这是一种常见的反模式行为。
从 设计模式 来讲,我们对可迭代对象并不只有逐个迭代这种方式,有可能跳跃式迭代,也有可能反向迭代。如果把一个对象设计成既是可迭代对象也是迭代器,那这个对象内部将会有成吨的 if-else
语句,这非常不利于维护和扩展。
3. 生成器
上述版本中的 Sentence
需要配备一个迭代器。而更符合Python风格的方式是用生成器函数代替 SentenceIterator
。
3.1 生成器函数
使用生成器函数改写传统的迭代器(实际上不再定义迭代器):
# 代码3.1 Sentence中其余代码不变,且不用再定义SentenceIterator class Sentence: -- snip -- def __iter__(self): for word in self.words: yield word 复制代码
解释: 这里的 __iter__
是 生成器函数 ,调用它时会创建 生成器对象**,然后用这个生成器对象充当迭代器。
3.2 生成器函数工作原理
只要Python函数的定义体中有 yield
关键字,该函数就是生成器函数 (这也是和普通函数的唯一区别)。“生成器”一词指代生成器函数,以及生成器函数构建的生成器对象,比较笼统,所以请具体语境具体分析。
生成器函数是一个生成器工厂,调用生成器函数时创建一个生成器对象, 包装生成器函数的定义体 。
生成器对象实现了迭代器接口,通常Python会自动创建这个对象。当对生成器对象调用 next()
函数时, 生成器函数 会 执行到 定义体中的下一个 yield
语句的末尾, 生成 yield
关键字后面的表达式的值,然后停止在此处,等待下一次调用。当定义体中 所有语句 都执行完后,生成器函数返回,外层的生成器对象抛出 StopIteration
异常。
友情提醒 :生成器函数并不是 只执行 其中的 yield
语句;也不是 只执行到 最后一个 yield
语句,如果最后一个 yield
语句后面还有代码,依然会执行。
下面是关于生成器的一个简单例子:
# 代码3.2 >>> def gen_AB(): ... print("Start") ... yield "A" ... print("Continue") ... yield "B" ... print("End.") ... >>> gen_AB <function gen_AB at 0x...> # 返回值和普通函数没区别 >>> gen_AB() <generator object gen_AB at 0x...> # 返回了一个生成器对象 >>> g = gen_AB() >>> next(g) Start # print("Start") 'A' # 这个是生成的值 >>> temp = next(g) # 获取生成器生成的第二个值 Continue # print("Continue") >>> temp # 输出生成器生成的第二个值 'B' # 此时还并没有抛出异常,因为生成器函数还没执行完 >>> next(g) End. # 生成器函数执行完毕,生成器抛出异常。 Traceback (most recent call last): # 显式调用next()需要自行捕获异常 File "<input>", line 1, in <module> StopIteration 复制代码
3.3 惰性实现与生成器表达式
上述的两个版本中,我们都用了 self.words
属性来保存文本中的单词,即在创建 Sentence
对象时就获得了所有的单词。这种方式叫做 及早求值 (Eager Evaluation)。而与之相反的则是 惰性求值 (Lazy Evaluation),通俗讲就是“等用到的时候再来求值”。及早求值可能会消耗大量内存,而惰性求值则是为了减少内存的使用。
生成器表达式以前提到过,它是用 圆括号 括起来的推导式(并不是生成元组)。 生成器表达式 可以理解为 列表推导 的 惰性 版本:不会一次性构造整个列表,而是返回一个生成器,按需惰性生成元素。以下是它的一个简单示例:
# 代码3.3 >>> def gen_AB(): ... print("Start") ... yield "A" ... print("Continue") ... yield "B" ... print("End.") ... >>> res1 = [x * 3 for x in gen_AB()] # 这里有一个生成器,但被列表推导式全部迭代完 Start Continue End. >>> res1 # 一次性生成了完整的列表 ['AAA', 'BBB'] >>> res2 = (x * 3 for x in gen_AB()) # 这里其实有连个生成器 >>> res2 # 返回了一个生成器对象,并没有一次性生成所有数据,惰性 <generator object <genexpr> at 0x000001D6D34D4408> >>> for i in res2: ... print(i) ... Start AAA Continue BBB End. 复制代码
***解释:***由于 gen_AB()
是个生成器函数,所以 (x * 3 for x in gen_AB())
包含了两个生成器对象,其中一个是由 gen_AB()
创建的,是不是有点嵌套生成器的意思?
现在我们使用 re.finditer
将第2版的 Sentence
改为惰性版本,并使用生成器表达式进一步简化代码:
# 代码3.4 class Sentence: def __init__(self, text): self.text = text # 去掉了self.words def __iter__(self): return (match.group() for match in RE_WORD.finditer(self.text)) # 不适用生成器表达式的版本如下: # for match in RE_WORD.finditer(self.text): # yield match.group() 复制代码
***友情提醒:***在Python3中,如果想把某种实现变成惰性版本,一般都是可以的......
生成器表达式是创建生成器的简洁语法,这样就无需定义生成器函数,一般在情况简单时使用。不过, 生成器函数 灵活得多,可以使用多个语句实现更复杂的逻辑,也可以作为 协程 使用,还可以重用代码。
3.4 itertools模块
该模块包含了很多有用的生成器函数,这里介绍两个生成器函数 itertools.count
和 itertools.takewhile
。
前面介绍的生成器中的数据都是有穷集合,而 itertools.count
则生成无穷集合。它有两个参数起始数值 start
和步长 step
, start
默认是 0
, step
默认是 1
。这两个参数都支持多种数字类型,比如 int
, float
, decimal.Decimal
和 fractions.Fraction
。以下是它的一个示例:
# 代码3.5 >>> import itertools >>> gen = itertools.count(1, 0.5) >>> next(gen) 1 >>> next(gen) 1.5 复制代码
由于 itertools.count
不停止生成数据,所以如果调用 list(count())
,你的电脑会疯狂运转,直到超出内存限制。
itertools.takewhile
函数则不同,它会生成一个使用另一个生成器的生成器,在指定的函数返回 False
时停止。因此,这两个迭代器可以结合使用:
# 代码3.6 >>> gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, 0.5)) >>> list(gen) [1, 1.5, 2.0, 2.5] 复制代码
标准库中还有很多非常有用的生成器函数,这里就不一一列出了。
3.5 yield from
如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套 for
循环,比如如下函数:
# 代码3.7 def chain(*iterables): # iterables中的元素是可迭代对象 for it in iterables: for i in it: yield i 复制代码
而如果使用 yield from
句法则可以使代码更简洁:
# 代码3.8 def chain(*iterables): for it in iterables: yield from it 复制代码
yield from
语法不仅仅是语法糖,除了代替循环之外, yield from
还会创建通道,把生成器当做协程使用。
3.6 把生成器当做协程
从Python2.5起,生成器加入了一个名为 .send()
的方法,与 .__next__
方法一样, .send
方法致使生成器推进到下一个 yield
语句。但 .send
方法还允许生成器的调用者向生成器传入参数,把这个参数作为对应的 yield
语句的返回值。这个方法让调用者和生成器之间能双向交换数据,而 .__next__
方法只允许调用者从生成器获取值。下面是这个方法的一个简单示例:
# 代码3.9 省略了最后抛出的StopIteration异常 >>> def test_send(): ... a = yield 1 ... print("At the end of function, a = ", a) ... >>> g = test_send() >>> next(g) 1 >>> next(g) At the end of function, a = None # 可以看出,yield表达式是有返回值的,默认返回None >>> g = test_send() # 新建一个生成器 >>> next(g) # 在调用send()之前,必须先至少调用过一次next() 1 >>> g.send("msg") At the end of function, a = msg # 把我们传入的参数作为了yield表达式的返回值 复制代码
这一项重要改进甚至改变了生成器的本性:像这样用的话,生成器就变为了协程。
这里是想提醒大家,请慎重使用这个方法!生成器用于生产供迭代的数据,协程是数据的消费者。为了避免不必要的麻烦,请严格区分协程和迭代,虽然协程也用到到了 yield
,但协程和迭代没有关系!
关于协程的内容将会在后面的文章中介绍。
4. 总结
本篇首先介绍了可迭代对象与迭代器,内容包括迭代的原理以及 iter()
和 next()
函数所做的工作,然后实现了一个经典的迭代器。随后,为了让这个经典的迭代器更符合Python风格,我们讨论了生成器。这期间讲到了生成器和迭代器的关系,生成器函数及其工作原理,惰性实现和生成器表达式。根据这些内容,我们将之前传统的迭代器进行了简化。随后补充了三个内容: itertools
模块中的生成器函数, yield from
语法和生成器的 .send()
。
最后,建议大家一定要多了解标准库中的生成器函数,尤其是 itertools
模块。
迎大家关注我的微信公众号"代码港" & 个人网站www.vpointer.net ~
以上所述就是小编给大家介绍的《Python学习之路34-迭代器和生成器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 可迭代对象,迭代器(对象),生成器(对象)
- 搞清楚 Python 的迭代器、可迭代对象、生成器
- 深入理解python的迭代器,生成器,可迭代对象区别
- 生成器与迭代器
- 迭代器和生成器
- 【重温基础】13.迭代器和生成器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C程序设计语言
Brian W. Kernighan、Dennis M. Ritchie / 机械工业出版社 / 2006-8-1 / 35.00元
在计算机发展的历史上,没有哪一种程序设计语言像C语言这样应用广泛。本书是C语言的设计者之一Dennis M.Ritchie和著名计算机科学家Brian W.Kernighan合著的一本介绍C语言的权威经典著作。我们现在见到的大量论述C语言程序设计的教材和专著均以此书为蓝本。本书第1版中介绍的C语言成为后来广泛使用的C语言版本——标准C的基础。人们熟知的“hello,World"程序就是由本书首次引......一起来看看 《C程序设计语言》 这本书的介绍吧!