前言
迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)
所有生成器都是迭代器,因为生成器完全实现了迭代器接口
在Python3 中,生成器有广泛的用途。现在,即使是内置的range()函数也返回一个类似
生成器的对象,而以前则返回完整的列表
iter()
解释器需要迭代对象x时,会自动调用iter(x)
.
内置的iter
函数有以下作用:
- 检查对象是否实现了
__iter__()
,如果实现了就调用它,从而获取一个迭代器 - 当
__iter__()
不存在时转而调用__getitem__()
, python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素 - 抛出
TypeError
, 提示object is not iterable
可迭代的对象与迭代器的对比
下面是一个简单的 for 循环,迭代一个字符串。这里,字符串ABC
是可迭代的对象。背后是有迭代器的,只不过我们看不到:
|
|
若使用while
, 使用可迭代的对象构建迭代器it
|
|
标准的迭代器接口有两个方法:
__next__()
, 返回下一个可用的元素,如果没有元素了则抛出StopIteration
异常__iter__()
, 返回self, 以便在应该使用可迭代对象的地方使用迭代器
这个接口在collections.abc.Iterator
抽象基类中制定。这个类定义了__next__()
抽象方法,而且继承自 Iterable
类;__iter__
抽象方法则在Iterable
类中定义, 如下图所示
【图片】
检查对象x是否为迭代器最好的方式是调用isinstance(x, abc.Iterator)
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. 即python里的迭代器不是类型,是协议
把Sentence变成迭代器:坏主意
原版Sentence
类
|
|
使用迭代器模式
|
|
迭代器模式可用来:
- 访问一个聚合对象的内容而无需暴露它的内部表示
- 支持对聚合对象的多种遍历
- 为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)
为了“支持多种遍历”,必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态,因此这一模式正确的实现方式是,每次调用iter(my_iterable)
都新建一个独立的迭代器
。
构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者
。要知道,可迭代的对象有个__iter__
方法,每次都实例化一个新的迭代器;而迭代器要实现__next__
方法,返回单个元素,此外还要实现__iter__
方法,返回迭代器本身。
ps:这里我就不明白了,对于迭代器的__iter__()
改个名字不久行了?
可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现__iter__
方法,但不能实现__next__
方法。
更符合Python
习惯的方式实现Sentence
类(使用生成器)
|
|
本书的审查Alex Martelli
建议这样编写__iter__
|
|
即利用words
(list
类型)的__iter__
来实现
生成器函数的工作原理
只要python函数的定义体中有yield
关键字, 该函数就是生成器函数。调用生成器函数时会返回一个生成器对象,也就是说生成器函数是生成器工厂
(我理解为生成器的工厂函数)(yield
理解为生产出
)
生成器不会以常规的方式“返回”值(即return
:生成器函数定义体中的return
语句会触发生成器对象抛出StopIteration
异常
一个例子
|
|
for
机制会捕获异常,因此循环终止时没有报错
在Python3.3
之前,如果生成器函数中的return
语句有返回值,那么会报错。现在可以这么做,不过return
语句
仍
会导致StopIteration
异常抛出。调用方可以从异常对象中获取返回值,不过只有把生成器函数当成协程使用
时,这么做才有意义
re.finditer
函数是re.findall
函数的惰性版本,返回的不是列表,而是一个生成器,按需生成re.MatchObject
实例。如果有很多匹配,re.finditer
函数能节省大量内存
生成器表达式
生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表,而是返回一个生成器,按需惰性生成元素。
使用生成器表达式实现Sentence
类
|
|
生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利
等差数列生成器
|
|
itertools
模块
使用itertools.count()
生成等差数列
|
|
itertools.takewhile()
itertools.takewhile(predicate, iterable)
, 对于iterable
中的元素,满足predicate
的条件,则结束,大致等于
|
|
利用takewhile()
实现之前的那两个例子(生成从0开始的12个偶数)
|
|
标准库中的生成器函数
第一组是用于过滤的生成器函数:从输入的可迭代对象中产出元素的子集,而且不修改元素本身
模块 | 函数 | 说明 |
---|---|---|
itertools |
compress(it, selector_it) |
并行处理两个可迭代的对象;如果selector_it 中的元素是真值,产出it 中对应的元素 |
itertools |
dropwhile(predicate, it) |
处理it , 跳过predicate 的计算结果为真值的元素,然后产出剩下的各个元素 |
内置 | filter(predicate, it) |
把it 中的各个元素传给predicate , 如果predicate(item) 返回True ,那么产出对应的元素 |
itertools |
filterfalse(predicate, it) |
predicate 返回假值时产出对应的元素 |
itertools |
islice(it, stop) 或者islice(it, start, stop, step=1) |
产出it 的切片, 作用类似于s[:stop] 或s[start:stop:step] , 不过it 可以是任何可迭代的对象, 而且这个函数实现的是惰性操作 |
itertools |
takewhile(predicate, it) |
predicate 返回真值时产出对应的元素,然后立即停止,不再继续 |
『图片』
第二组生成器函数会从输入的可迭代对象中的各个元素中产出一个元素。如果输入来自多个可迭代的对象,第一个可迭代的对象到头后就停止输出。
模块 | 函数 | 说明 |
---|---|---|
itertools |
accumulate(it, [func]) |
产出累积的总和; 如果提供了func , 那么把前两个元素传给它, 然后把计算结果和下一个元素传给它,以此类推,最后产出结果 |
内置 | enumerate(iterable, start=0) |
产出由两个元素组成的元组,结构是(index, item) , 其中index 从start 开始计数, item 则从iterable 中获取 |
内置 | map(func, it1,[it2, ..., itN]) |
把it 中的各个元素传给func ,产出结果;如果传入N个可迭代的对象,那么func 必须能接受 N 个参数,而且要并行处理各个可迭代的对象 |
itertools |
starmap(func, it) |
把it 中的各个元素传给func ,产出结果;输入的可迭代对象应该产出可迭代的元素 iit ,然后以 func(*iit) 这种形式调用 func |
『图片』
第三组是用于合并的生成器函数,这些函数都从输入的多个可迭代对象中产出元素。chain
和chain.from_iterable
按顺序(一个接一个)处理输入的可迭代对象,而product
、zip
和zip_longest
并行处理输入的各个可迭代对象
模块 | 函数 | 说明 |
---|---|---|
itertools |
chain(it1, ..., itN) |
先产出it1 中的所有元素,然后产出it2 中的所有元素,以此类推,无缝连接在一起` |
itertools |
chain.from_iterable(it) |
产出it 生成的各个可迭代对象中的元素,一个接一个,无缝连接在一起; it 应该产出可迭代的元素,例如可迭代的对象列表 |
itertools |
product(it1, ..., itN, repeat=1) |
计算笛卡儿积, repeat 指明重复处理多少次输入的可迭代对象 |
内置 | zip(it1, ..., itN) |
并行从输入的各个可迭代对象中获取元素,产出由 N 个元素组成的元组,只要有一个可迭代的对象到头了,就默默地停止 |
itertools |
zip_longest(it1, ...,itN, fillvalue=None) |
并行从输入的各个可迭代对象中获取元素,产出由N个元素组成的元组,等到最长的可迭代对象到头后才停止,空缺的值使用fillvalue 填充 |
第四组生成器函数会从一个元素中产出多个值,扩展输入的可迭代对象
模块 | 函数 | 说明 |
---|---|---|
itertools |
combinations(it, out_len) |
把it 产出的out_len 个元素组合在一起(数学意义上的组合),然后产出 |
itertools |
combinations_with_replacement(it,out_len) |
把it 产出的out_len 个元素组合在一起,然后产出,包含相同元素的组合` |
itertools |
count(start=0, step=1) |
从start 开始不断产出数字,按step 指定的步幅增加 |
itertools |
cycle(it) |
从it 中产出各个元素, 存储各个元素的副本,然后按顺序重复不断地产出各个元素 |
itertools |
permutations(it, out_len=None) |
把out_len 个it 产出的元素排列在一起,然后产出这些排列; out_len 的默认值等于len(list(it)) (即生成数学意义上的排列,长度为out_len ) |
itertools |
repeat(item, [times]) |
重复不断地产出指定的元素,除非提供times 指定次数 |
第五组生成器函数用于产出输入的可迭代对象中的全部元素,不过会以某种方式重新排列
模块 | 函数 | 说明 |
---|---|---|
itertools |
groupby(it,key=None) |
产出由两个元素组成的元素,形式为(key, group) , 其中key是分组标准, group是生成器,用于产出分组里的元素(假定输入的可迭代对象要使用分组标准排序) |
内置 | reversed(seq) |
从后向前,倒序产出 seq 中的元素; seq 必须是序列,或者是实现了__reversed__ 特殊方法的对象 |
itertools |
tee(it, n=2) |
产出一个由 n 个生成器组成的元组,每个生成器用于单独产出输入的可迭代对象中的元素 |
成功抄吐了,更多详情请看文档,我都怀疑我为什么要写这没营养的读书笔记了
yield from
实现自己的chain
生成器
|
|
chain
生成器函数把操作依次交给接收到的各个可迭代对象处理。为此,PEP 380 — Syntax for Delegating to a Subgenerator
(https://www.python.org/dev/peps/pep-0380/)引入了一个新句法(翻译为委托给子生成器)
|
|
可以看出, yield from i
完全代替了内层的for
循环. 除了代替循环之外, yield from
还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。把生成器当成协程使用时,这个通道特别重要,不仅能为客户端代码生成值,还能使用客户端代码提供的值(16章再继续讲)
ps现在再看yield from
, yield from i
是把i
自动生成了一个生成器?
深入分析iter函数
iter
数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是一个key,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常(可以理解为产生这个值就抛出异常并暂停)
|
|
用随机数函数d6
生成随机数生成器dg
,不断的next(dg)
直到遇到x=1
抛出StopIteration
(for
循环会pass
这个异常)
利用这一功能,这里有个实用的例子,这段代码逐行读取文件,直到遇到空行或者到达文件末尾为止:
|
|
ps: 只是一种启发
把生成器当协程
与__next__()
方法一样,send()
方法致使生成器前进到下一个yield
语句。不同的是,send()
方法还允许使用生成器的客户把数据发给自己,即不管传给send()
方法什么参数,那个参数都会成为生成器函数定义体中对应的yield
表达式的值。这意味着, send()
方法允许在客户代码和生成器之间双向交换数据。而__next__()
方法只允许客户从生成器中获取数据。这是一项重要的“改进”,甚至改变了生成器的本性:像这样使用的话,生成器就变身为协程
。
- 生成器用于生成供迭代的数据
- 协程是数据的消费者
- 为了避免脑袋炸裂,不能把这两个概念混为一谈
- 协程与迭代无关(注意,虽然在协程中会使用
yield
产出值,但这与迭代无关