小结
- 共计 61页
本章速读
补充知识点
- close
生成器的 close 方法也比较简单,就是手动关闭这个生成器,关闭后的生成器无法再进行操作。
g.close()
- yield关键字的正确使用&适用场景
- 超大集合(超出物理机的内存上限)的生成,生成器只有在执行到 yield 时才会迭代数据,这时只会申请需要返回元素的内存空间,后面的内存占用可以得到释放
- 简化代码结构:如果一个方法要返回一个 list,但这个 list 是多个逻辑块组合后才能产生的
- 协程与并发,多线程的方式编写程序代码,最常用的编程模型就是「生产者-消费者」模型,即一个进程 / 线程生产数据,其他进程 / 线程消费数据。
引用自: Python进阶——如何正确使用yield?
Kaito,https://zhuanlan.zhihu.com/p/321302488
14.1 Sentence类第1版:单词序列
序列可以迭代的原因:iter函数
解释器需要迭代对象 x 时,会自动调用 iter(x)。
内置的 iter函数有以下作用。
(1) 检查对象是否实现了 iter 方法,如果实现了就调用它,获取 一个迭代器。
(2) 如果没有实现 iter 方法,但是实现了 getitem 方法, Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。
(3) 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“C object is not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类。
14.2 可迭代的对象与迭代器的对比
可迭代的对象
- 使用 iter 内置函数可以获取迭代器的对象。
- 如果对象实现了能返 回迭代器的 __iter__ 方法,那么对象就是可迭代的。
- 序列都可以迭代;实现了 __getitem__ 方法,而且其参数是从零开始的索引,这种对象也可以迭代。
可迭代的对象和迭代器之间的关系:
Python 从可迭代的对象中获取迭代器
使用for/while循环简单模拟迭代器
下面是一个简单的 for 循环,迭代一个字符串。这里,字符串 'ABC' 是可迭代的对象。背后是有迭代器的,只不过我们看不到:
s = 'ABC'
for char in s:
print(char)
A
B
C
it = iter(s)
while True:
try:
print(next(it))
except StopIteration:
del it
break
A
B
C
StopIteration 异常表明迭代器到头了。Python 语言内部会处理 for 循环和其他迭代上下文(如列表推导、元组拆包,等等)中的 StopIteration 异常。
标准的迭代器接口中有两个方法:
-
next
-
iter
14.4 Sentence类第3版:生成器函数
- 生成器函数都不会抛出 StopIteration 异常,而是在生成完全部值之后会直接退出
import re
RE_WORD = re.compile('w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __iter__(self):
for word in self.words:
yield word
return
# 测试
sen = Sentence('233 2333')
gentor = sen.__iter__()
print(next(gentor)) # 233
print(next(gentor)) # 2333
print(next(gentor)) # StopIteration
print(next(gentor))
233
2333
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-8-baaa7e5b37df> in <module>
1 print(next(gentor))
2 print(next(gentor))
----> 3 print(next(gentor))
4 print(next(gentor))
StopIteration:
生成器函数的工作原理
只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函 数。
调用生成器函数时,会返回一个生成器对象。
也就是说,生成器函 数是生成器工厂。
有时,我会在生成器函数的名称中加上 gen 前缀或后缀,不过这不是习惯做法。显然,如果 实现的是迭代器,那就不能这么做,因为所需的特殊方法必须命名为 iter。 -- Guido
def gen_123():
yield 1
yield 2
yield 3
gen_123()
<generator object gen_123 at 0x0000020B047D5EC8>
g = gen_123()
print(next(g))
print(next(g))
print(next(g))
print(next(g))
1
2
3
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-13-7295ea4d5aaf> in <module>
3 print(next(g))
4 print(next(g))
----> 5 print(next(g))
6
StopIteration:
简单裂解生成器 (yield暂停,调用一次暂停一次)
生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成 器传给 next(...) 函数时,生成器函数会向前,执行函数定义体中的 下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂 停。最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。
使用 for 循环更清楚地说明了生成器函数定义体的执行过 程。
按需惰性生成元素,每次调用再生成一次
def gen_nums():
print('start...')
yield 'A'
print('continue')
yield 'B'
print('end.')
for c in gen_nums():
print('-->', c)
start...
--> A
continue
--> B
end.
14.5 Sentence类第4版:惰性实现
import re
RE_WORD = re.compile('w+')
class Sentence:
def __init__(self, text):
self.text = text
def __iter__(self):
for match in RE_WORD.findall(self.text):
yield match.group() # match.group() 方法从 MatchObject 实例中提取匹配正则表达式的 具体文本。
sen = Sentence('a b c d e f g')
for x in sen:
print(x)
a
b
c
d
e
f
g
14.6 Sentence类第5版:生成器表达式
如果列表推导是 制造列表的工厂,那么生成器表达式就是制造生成器的工厂
def gen_nums():
print('start...')
yield 'A'
print('continue')
yield 'B'
print('end.')
gen_lst = [x*3 for x in gen_nums()]
start...
continue
end.
for i in gen_lst:
print(i)
AAA
BBB
14.7 何时使用生成器表达式
见小结的补充.
14.8 另一个示例:等差数列生成器
使用itertools模块生成等差数列
例如,itertools.count 函数返回的生成器能生成多个数。
如果不传 入参数,itertools.count 函数会生成从零开始的整数数列。不过, 我们可以提供可选的 start 和 step 值,这样实现的作用与 aritprog_gen 函数十分相似:
import itertools
gen = itertools.count(1, .5)
next(gen)
1
next(gen)
1.5
# list(count()) 是个大坑
不过,itertools.takewhile 函数则不同,它会生成一个使用另一个 生成器的生成器,在指定的条件计算结果为 False 时停止。因此,可 以把这两个函数结合在一起使用,编写下述代码:
gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5))
list(gen)
[1, 1.5, 2.0, 2.5]
14.9 标准库中的生成器函数
- 有用于逐行迭代纯文本文件的对象,还有出 色的 os.walk 函数;这个函数在遍历目录树的过程中产出文件名,因此递归搜索文件系统像for循环那样简单。
- reverse(),返回的也是个生成器
- itertools.groupby(it, key=None); 产出由两个元素组成的元素,形式为 (key, group),其中 key 是分组标准,group 是生成器, 用于产出分组里的元素
更多,略。
14.10 Python 3.3中新出现的句法:yield from
类似于语法糖,yield from i 完全代替了内层的 for 循环;但是在后面的章节中,还可以yield from 还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。把生成器当成协程使用时,这个 通道特别重要,不仅能为客户端代码生成值,还能使用客户端代码提供 的值。
如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使 用嵌套的 for 循环。
例如,下面是我们自己实现的 chain 生成器:
def chain(*iterables):
for it in iterables:
for i in it:
yield i
s = 'ABC'
t = tuple(range(3))
list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]
def chain(*iterables):
for i in iterables:
yield from i
list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]
14.11 可迭代的归约函数(类似于reduce的实现之一)
表 14-6 中的函数都接受一个可迭代的对象,然后返回单个结果。这些 函数叫“归约”函数、“合拢”函数或“累加”函数。其实,这里列出的每个 内置函数都可以使用 functools.reduce 函数实现,内置是因为使用 它们便于解决常见的问题。
表14-6:
- 内置, all(it),可短路、it 中的所有元素都为真值时返回 True,否则返回 False; all([]) 返回 True
- 内置,any(it), 只要 it 中有元素为真值就返回 True,否则返回 False; any([]) 返回 False
- (内置), max(it, [key=,] [default=]), 返回 it 中值最大的元素;*key 是排序函数,与 sorted 函 数中的一样;如果可迭代的对象为空,返回 default
- (内置), min(it, [key=,] [default=]) ,返回 it 中值最小的元素;#key 是排序函数,与 sorted 函 数中的一样;如果可迭代的对象为空,返回 default
- functools reduce(func, it, [initial]), 把前两个元素传给 func,然后把计算结果和第三个元素传 给 func,以此类推,返回最后的结果;如果提供了 initial,把它当作第一个元素传入。
- (内置) sum(it, start=0), it 中所有元素的总和,如果提供可选的 start,会把它加 上(计算浮点数的加法时,可以使用 math.fsum 函数提高 精度
其他的:
- sorted 会构建并 返回真正的列表
14.12 深入分析iter函数 (使用哨符,掷骰子)
- iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规 的函数或任何可调用的对象创建迭代器。
- 这样使用时,第一个参数必须 是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值 是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛 出 StopIteration 异常,而不产出哨符。
下述示例展示如何使用 iter 函数掷骰子,直到掷出 1 点为止:
from random import randint
def d6():
return randint(1, 6) # Return random integer in range [a, b], including both end points.
d6_iter = iter(d6, 6)
d6_iter
<callable_iterator at 0x20b03d96788>
for roll in d6_iter:
print(roll)
4
3
2
14.14 把生成器当成协程——允许双方交换数据
与 .next() 方法一样,.send() 方法致使生成器前进到下一个 yield 语句。不过,.send() 方法还允许使用生成器的客户把数据发给 自己,即不管传给 .send() 方法什么参数,那个参数都会成为生成器 函数定义体中对应的 yield 表达式的值。
也就是说,.send() 方法允许在客户代码和生成器之间双向交换数据。而 .next() 方法只允许客户从生成器中获取数据。
这是一项重要的“改进”,甚至改变了生成器的本性:像这样使用的话, 生成器就变身为协程。在 PyCon US 2009 期间举办的一场著名的课程中 (http://www.dabeaz.com/coroutines/),David Beazley(可能是 Python 社 区中在协程方面最多产的作者和演讲者)提醒道:
- 生成器用于生成供迭代的数据
- 协程是数据的消费者
- 为了避免脑袋炸裂,不能把这两个概念混为一谈
- 协程与迭代无关
注意,虽然在协程中会使用 yield 产出值,但这与迭代无关。
第 16 章会讨论协程。