可迭代的对象、迭代器和生成器
此笔记记录于《流畅的 python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为 jupyter 转的 markdown,原始版 jupyter 笔记在这个仓库
当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号,(至少对我来说)表明我对问题的抽象还不够深——这通常意味着自己正在手动完成的事情,本应该通过写代码来让宏的扩展自动实现。
迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)。
所有生成器都是迭代器,因为生成器完全实现了迭代器接口。不过,根据《设计模式:可复用面向对象软件的基础》一书的定义,迭代器用于从集合中取出元素;而生成器用于“凭空”生成元素。
Sentence 类第 1 版:单词序列
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 __getitem__(self, index):
return self.words[index]
def __len__(self):
return len(self.words)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
s = Sentence('`The time has come, ` the Walrus said,')
s
Sentence('`The time ha... Walrus said,')
for word in s:
print(word)
The
time
has
come
the
Walrus
said
list(s)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
序列可以迭代的原因:iter 函数,解释器需要迭代对象 x 时,会自动调用 iter(x)。内置的 iter 函数有以下作用:
- 检查对象是否实现了
__iter__
方法,如果实现了就调用它,获取一个迭代器。 - 如果没有实现
__iter__
方法,但是实现了__getitem__
方法,Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。 - 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“C object is not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类。
任何 Python 序列都可迭代的原因是,它们都实现了
__getitem__
方法。其实,标准的序列也都实现了__iter__
方法,因此你也应该这么做。之所以对__getitem__
方法做特殊处理,是为了向后兼容,而未来可能不会再这么做
在白鹅类型(goose-typing)理论中,可迭代对象的定义简单一些,不过没那么灵活:如果实现了__iter__
方法,那么就认为对象是可迭代的。此时,不需要创建子类,也不用注册,因为abc.Iterable
类实现了__subclasshook__
方法;
不过要注意,虽然前面定义的 Sentence 类是可以迭代的,但却无法通过 issubclass (Sentence,abc.Iterable)测试。
class Foo:
def __iter__(self):
pass
from collections import abc
issubclass(Foo, abc.Iterable)
True
f = Foo()
isinstance(f, abc.Iterable)
True
从 Python 3.4 开始,检查对象 x 能否迭代,最准确的方法是:调用 iter(x)函数,如果不可迭代,再处理 TypeError 异常。这比使用
isinstance(x, abc.Iterable)
更准确,因为 iter(x)函数会考虑到遗留的__getitem__
方法,而abc.Iterable
类则不考虑。
可迭代对象与迭代器的对比
可迭代的对象:使用 iter 内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的__iter__
方法,那么对象就是可迭代的。序列都可以迭代;实现了__getitem__
方法,而且其参数是从零开始的索引,这种对象也可以迭代。
我们要明确可迭代的对象和迭代器之间的关系:Python 从可迭代的对象中获取迭代器。
# 字符串'ABC'是可迭代的对象。背后是有迭代器的,只不过我们看不到
s = 'ABC'
for char in s:
print(char)
A
B
C
s = 'ABC'
it = iter(s) # 使用可迭代的对象构建迭代器 it
while True:
try:
print(next(it)) # 不断在迭代器上调用 next 函数,获取下一个字符
except StopIteration:
del it
break
A
B
C
标准的迭代器接口有两个方法:
__next__
: 返回下一个可用的元素,如果没有了,抛出 StopIteration 异常__iter__
: 返回 self,以便在应该使用可迭代对象的地方使用迭代器,例如在 for 循环中
# abc.Iterator 类,摘自 Lib/_collections_abc.py
class Iterator(Iterable):
__slots__ = ( )
@abstractmethod
def __next__(self):
'Return the next item from the iterator. When exhausted, raise StopIteration'
raise StopIteration
def __iter__(self):
return self
@classmethod
def __subclasshook__(cls, C):
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
return NotImplemented
迭代器是这样的对象:实现了无参数的__next__
方法,返回序列中的下一个元素;如果没有元素了,那么抛出 StopIteration 异常。Python 中的迭代器还实现了__iter__
方法,因此迭代器也可以迭代。
因为内置的iter(...)
函数会对序列做特殊处理,所以第 1 版 Sentence 类可以迭代。
Sentence 类第 2 版:典型的迭代器
使用迭代器模式实现 Sentence 类
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 __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.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
与前一版相比,这里只多了一个__iter__
方法。这一版没有__getitem__
方法,为的是明确表明这个类可以迭代,因为实现了__iter__
方法。
把 Sentence 变成迭代器:坏主意:
构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对象有个__iter__
方法,每次都实例化一个新的迭代器;而迭代器要实现__next__
方法,返回单个元素,此外还要实现__iter__
方法,返回迭代器本身。
因此,迭代器可以迭代,但是可迭代的对象不是迭代器。
所以这里并没有直接在 Sentence 类上实现迭代器模式,而是在迭代器类 SentenceIterator 中实现了迭代器模式。
Sentence 类第 3 版:生成器函数
实现相同功能,但却符合 Python 习惯的方式是,用生成器函数代替 SentenceIterator 类。
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 __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for word in self.words:
yield word # 返回,但当前状态会被记住
return # 这个 return 语句不是必要的;这个函数可以直接“落空”,自动返回。不管有没有 return 语句,生成器函数都不会抛出 StopIteration 异常,而是在生成完全部值之后会直接退出
# 完成!➍
Sentence 类第 4 版:惰性实现
只要使用的是 Python 3,思索着做某件事有没有懒惰的方式,答案通常都是肯定的。
re.finditer
函数是re.findall
函数的惰性版本,返回的不是列表,而是一个生成器,按需生成re.MatchObject
实例。如果有很多匹配,re.finditer
函数能节省大量内存。
我们要使用这个函数让第 4 版 Sentence 类变得懒惰,即只在需要时才生成下一个单词。
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text # 不再需要 words 列表。
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
for match in RE_WORD.finditer(self.text): # finditer 函数构建一个迭代器,包含 self.text 中匹配 RE_WORD 的单词,产出 MatchObject 实例。
yield match.group() # match.group( )方法从 MatchObject 实例中提取匹配正则表达式的具体文本。
Sentence 类第 5 版:生成器表达式
def gen_AB():
print('start')
yield 'A'
print('continue')
yield 'B'
print('end.')
res2 = (x*3 for x in gen_AB())
res2 # 这种语法是可以产生生成器的,因此可以使用生成器表达式进一步减少 Sentence 类的代码
<generator object <genexpr> at 0x0000013742024D60>
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)'%reprlib.repr(self.text)
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
# 语法糖,可以不使用 yield 了
itertools 模块提供了 19 个生成器函数,结合起来使用能实现很多有趣的用法。
import itertools
gen = itertools.count(1, .5)
next(gen)
1
next(gen)
1.5
next(gen)
2.0
next(gen)
2.5
标准库中的生成器函数
函数名 | 描述 |
---|---|
enumerate | 枚举,返回一个枚举对象。其__next__()方法返回一个元组,包含一个计数(从 start 开始)和通过迭代 iterable 得到的值。 |
iter | 返回一个迭代器对象。 |
next | 返回迭代器的下一个项目。 |
filter | 构造一个迭代器,从 iterable 中过滤出一些元素,其元素使得 function 返回 true。 |
map | 返回一个迭代器,该迭代器通过对 iterable 中的每个元素应用 function 函数产生结果。 |
range | 虽然在 Python 3.x 中不是一个生成器函数,但 range 产生的是一个惰性序列,而不是列表。 |
zip | 使得多个 iterables 可以并行迭代,返回一个元组的迭代器。 |
reversed | 返回一个反向的迭代器。 |
sorted | 返回一个根据 iterable 中的项目排序的新列表,不是生成器函数,但生成的结果是迭代的。 |
Python 3.3 中新出现的句法:yield from
如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的 for 循环。
def chain(*iterables):
for it in iterables:
for i in it:
yield i
# 简化
def chain(*iterables):
for i in iterables:
yield from i
可迭代的归约函数
函数名 | 描述 |
---|---|
all | 如果 iterable 的所有元素都为真(或 iterable 为空),返回 True。 |
any | 如果 iterable 中有任何元素为真,返回 True。 |
sum | 对 iterable 中的项求和并返回总和。 |
max | 返回 iterable 中的最大值,或者两个及以上参数中的最大值。 |
min | 返回 iterable 中的最小值,或者两个及以上参数中的最小值。 |
reduce | 对 iterable 的元素累积应用两参数函数,从左到右,以减少 iterable 到单一值。 |
深入分析 iter 函数
iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。
from random import randint
def d6():
return randint(1, 6)
d6_iter = iter(d6, 1)
d6_iter
<callable_iterator at 0x1374201b2e0>
for roll in d6_iter:
print(roll)
4
2
4
3