大约第三次看迭代器和生成器了,之前一直看得云里雾里,今天还是来彻底了断下。2022 年 5 月 2 日回来复习,比之前理解的好了一些。
生成器
在这之前,很多人没了解过 yield
关键字,先通过 yield
关键字实现一个支持反向迭代的类(这只是一个类,和生成器无关):
1 | class Countdown: |
在了解 yield
的用法之后,来看一下生成器:
- 生成器函数是一种特殊的函数;如果一个函数包含
yield
表达式,那么它是一个生成器函数;调用它会返回一个生成器。即将普通函数的return
换成了yield
,仅在访问时有返回值,节省空间。但与普通函数不同的是,生成器函数返回的生成器只能用于迭代操作。 - 生成器表达式也能创建生成器,如:
g = (x for x in range(12))
,g
就是生成器,相当于把列表推导式的中括号换成了小括号,避免实例化。
1 | def countdown(n): |
现在假设,我们建立一个函数,这个函数会返回一个巨大的列表,而我们需要逐个访问。那么,如果函数生产列表中的每一个元素都需要耗费非常多的时间,或者生成所有元素需要等待很长时间,且,操作列表时会浪费巨大的内存。但使用 yield
把函数变成一个生成器函数,每次只产生一个元素,就能节省很多时间和空间的开销了。
常见的还有yield from
关键字,关键字后的参数是一个可迭代对象,可以简单理解为如下的替换:
1 | yield from x |
迭代器
先区分下容易搞混的一些概念:
- 迭代器是一种对象,如
iter(f)
后得到迭代器,可以next()
来读取下一个元素; - 迭代是一种操作,如
for
循环访问列表元素,可以理解为迭代; - 可迭代是对象的一种特性,如字典、列表和字符串等;
- 迭代器协议指的是类需要包含一个方法
__iter__
,该方法能返回一个能够逐个访问容器内所有元素的迭代器,则该容器类实现了迭代器协议。
那么我们可以根据概念先做一些简单的实验:
1 | # 错误,列表不是迭代器,是可迭代对象 |
先得到一个简单的结论,可作用于 next()
函数的对象都是迭代器类型,它们表示一个惰性计算(不在创建的时候计算所有值,而是在需要的时候才计算)的序列。
继续来看一段简单的代码,一个类提供了 __iter__
方法,该方法能返回一个能逐步访问类中所有元素的迭代器,实现了迭代器协议,那么可以说把一个类作为一个迭代器,实例化的对象就是可迭代对象。
1 | # __iter__ 方法支持迭代 |
稍微总结下,如果一个类想被用于 for ... in
循环,类似 list
那样,就必须实现一个 __iter__
方法,该方法返回一个迭代器。然后 for
循环就会通过 next(迭代器)
不断调用迭代对象的 __next__
方法拿到循环的下一个值,直到遇到 StopIteration
错误时退出循环。以斐波那契数列为例,写一个 Fib
类并实例化一个可迭代对象,不在依赖列表的迭代方法,可以作用于 for
循环:
1 | class Fib(object): |
这里有个小细节,iter(a)
的类型并不是迭代器类型,因为 iter(a)
调用 __iter__
方法返回的是对象本身,也就是 Fib
这个类。因此,iter()
的结果并不一定是迭代器类型,但一定可迭代。
此外,我们还可以反向迭代,但要求对象的大小预先可以确定或者对象实现了 __reversed__()
的特殊方法
1 | # 对象的大小可预先确定或者对象实现了 __reversed__() 的特殊方法 |
如果两者都不符合,那必须先将对象转换为一个列表。要注意的是如果可迭代对象元素很多的话,将其预先转换为一个列表要消耗大量的内存。
1 | f = open('somefile') |
总结
最后总结一下,生成器中 __iter__
和 __next__
这两个函数都不需要显式实现,而是由生成器自动提供,可以让代码更 pythonic,也更易读。任何一个生成器都会定义一个名为 __next__
的方法,这个方法要么返回迭代的下一项,要么引起结束迭代的异常 StopIteration
。生成器也是用在迭代操作中,因此它有和迭代器一样的特性,因此我们可以说生成器是迭代器,迭代器不一定是生成器。
而迭代器必须同时实现 __iter__
和 __next__
方法,而迭代时的 next()
函数的本质就是调用对象的 __next__
,__next__
方法包含了用户自定义的推导算法,这是迭代器对象的本质。
结合上面的总结做一个小例子,如果需要在生成器函数中访问其他属性时,可以将它实现为一个类,这个类将 __iter__
方法实现为生成器函数,而生成器函数会再次自动提供 __iter__
和 __next__
,因此,这个类的可迭代对象不再需要 __next__
方法。大型套娃现场。
1 | from collections import deque |
参考
站在巨人的肩膀上,我们能更好的前行。