《Fluent Python》 CH.14_控制流程_可迭代的对象、迭代器 和生成器 (yield关键字、iter函数、生成器工作原理)

小结

  • 共计 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 章会讨论协程。

你不逼自己一把,你永远都不知道自己有多优秀!只有经历了一些事,你才会懂得好好珍惜眼前的时光!
原文地址:https://www.cnblogs.com/zhazhaacmer/p/14464790.html