当前位置: 代码迷 >> 综合 >> Fluent Python - Part14 可迭代的对象、迭代器 和生成器
  详细解决方案

Fluent Python - Part14 可迭代的对象、迭代器 和生成器

热度:66   发布时间:2023-10-21 05:17:25.0

迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一个惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式。

在Python语言内部,迭代器用于支持:

  • for 循环
  • 构建和扩展集合类型
  • 逐行遍历文本文件
  • 列表推导,字典推导和集合推导
  • 元祖拆包
  • 调用函数时,使用 * 拆包实参

本章涵盖以下话题:

  • 语言内部使用 iter(...) 内置函数处理可迭代对象的方式
  • 如何使用 Python 实现经典的迭代器模式
  • 详细说明生成器函数的工作原理
  • 如何使用生成器函数或生成器表达式代替经典的迭代器
  • 如何使用标准库中通用的生成器函数
  • 如何使用 yield from 语句合并生成器
  • 为什么生成器和协程看似相同,实则差别很大,不能混淆

Sentence 类第1版:单词序列

我们要实现一个 Sentence 类,以此打开探索可迭代对象的旅程。我们向这个类的构造方法传入包含一些文本的字符串,然后可以逐个单词迭代。第1版要实现序列协议,这个类的对象可以迭代,因为所有序列都可以迭代—这一点前面已经说过,不过现在要说明真正的原因。

import re
import reprlibRE_WOR = re.compile('\w+')class Sentence:def __init__(self, text):self.text = textself.words = RE_WOR.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,')
print(s)for word in s:print(word)
print(list(s))
""" output: Sentence('"The time ha... Walrus said,') The time has come the Walrus said ['The', 'time', 'has', 'come', 'the', 'Walrus', 'said'] """

序列可以迭代的原因:iter 函数

解释器需要迭代对象 x 时,会自动调用 iter(x).

内置的 iter 函数有以下作用。

  1. 检查对象是否实现了 __iter__ 方法,如果实现了就调用它,获取一个迭代器。
  2. 如果没有实现 __iter__ 方法,但是实现了 __getitem__ 方法, Python 会创建一个迭代器,尝试按顺序(从索引0开始)获取元素。
  3. 如果尝试失败,Python 抛出 TypeError 异常, 通常会提示”C object is not iterable“。

任何Python序列都可迭代的原因是,它们都实现了 __getitem__ 方法。其实,标准的序列也都实现了 __iter__ 方法,因此你也应该这么做。之所以对 __getitem__ 方法做特殊处理,是为了向后兼容,而未来可能不会再这么做

在鸭子类型(duck typing) 的极端形式:不仅要实现特殊的 __iter__ 方法,还要实现 __getitem__ 方法,而且 __getitem__ 方法的参数是从0开始的整数(int),这样才认为对象是可迭代的。

在白鹅类型(goose-typing) 理论中,可迭代对象的定义简单一些,不过没那么灵活:如果实现了__iter__方法,那么就认为对象是可迭代的。此时,不需要创建子类,也不用注册,因为 abc.Iterable 类实现了 __subclasshook__ 方法。

可迭代的对象与迭代器的对比

我们要明确可迭代的对象和迭代器之间的关系:Python 从可迭代的对象中获取迭代器。

下面是一个简单的 for 循环,迭代一个字符串。这里,字符串 ‘ABC’ 是可迭代的对象。背后是有迭代器的,只不过我们看不到:

>>> s = 'ABC'
>>> for char in s:
...     print(char)
...
A
B
C

如果没有 for 语句,不得不使用 while 循环模拟,要像下面这样写:

>>> s = 'ABC'
>>> it = iter(s)
>>> while True:
...     try:
...             print(next(it))
...     except StopIteration:
...             del it
...             break
...
A
B
C

标准的迭代器接口有两个方法。

  • __next__: 返回下一个可用的元素,如果没有元素了,抛出 StopIteration 异常。
  • __iter__: 返回 self, 以便在应该使用可迭代对象的地方使用迭代器,例如在 for 循环中。

这个接口在 collections.abc.Iterator 抽象基类中制定。这个类定义了 __next__ 抽象方法,而且继承自 Iterable 类;__iter__ 抽象方法则在 Iterable 类中定义。
Fluent Python - Part14 可迭代的对象、迭代器 和生成器

Iterable 和 Iterator 抽象基类。以斜体显示的是抽象方法。以斜体显示的抽象方法。具体的 Iterable.__iter__ 方法应该返回一个 Iterator 实例。具体的 Iterator 类必须实现 __next__ 方法。Iterator.__iter__ 方法直接返回实例本身

根据本届内容,可以得出迭代器的定义如下:

  • 迭代器:实现了无参数的 __next__ 方法,返回序列中的下一个元素;如果没有元素了,那么抛出 StopIteration 异常。Python中的迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代。

因为内置的iter(...)函数会对序列做特殊处理,所以第1版Sentence 类可以迭代。接下来要实现标准的可迭代协议。

Sentence 类第2版:典型的迭代器

import re
import reprlibRE_WOR = re.compile('\w+')class Sentence:def __init__(self, text):self.text = textself.words = RE_WOR.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 = wordsself.index = 0def __next__(self):try:word = self.words[self.index]except IndexError:raise StopIteration()self.index += 1return worddef __iter__(self):return self
  • 注意:构建可迭代的对象迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对象有个 __iter__ 方法,每次都实例化一个新的迭代器;而迭代器要实现 __next__ 方法,返回单个元素,此外还要实现 __iter__ 方法,返回迭代器本身。

    因此,迭代器可以迭代,但是可迭代的对象不是迭代器。

    可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现 __iter__ 方法,但不能实现 __next__ 方法。

    另一方面,迭代器应该一直可以迭代。迭代器的 __iter__ 方法应该返回自身。

下一节将展示如何使用更符合 Python 习惯的方式实现 Sentence 类。

Sentence类第3版:生成器函数

实现相同功能,但却符合Python习惯的方式是,用生成器函数代替 SentenceIterator 类。

import re
import reprlibRE_WOR = re.compile('\w+')class Sentence:def __init__(self, text):self.text = textself.words = RE_WOR.findall(text)def __repr__(self):return 'Sentence(%s)' % reprlib.repr(self.text)def __iter__(self):for word in self.words:yield wordreturn 

第二版的 Sentence 类中, __iter__ 方法调用 SentenceIterator 类的构造方法创建一个迭代器并将其返回。而在该示例中,迭代器其实是生成器对象,每次调用 __iter__ 方法都会自动创建,因此这里的 __iter__ 方法是生成器函数。

生成器函数的工作原理

只要 Python 函数的定义体中有 yield 关键字,该函数都是生成器函数。调用生成器函数时,会返回一个生成器对象。也就是说,生成器函数时生成器工厂。

一个例子

>>> def gen_123():
...     yield 1
...     yield 2
...     yield 3
...
>>> gen_123
<function gen_123 at 0x10ec9bc80>
>>> gen_123()
<generator object gen_123 at 0x10eca1910>
>>> for i in gen_123():
...     print(i)
...
1
2
3
>>> g = gen_123()
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
Traceback (most recent call last):File "<stdin>", line 1, in <module>
StopIteration

生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给 next(...) 函数时,生成器函数会向前,执行函数定义体中的下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂停。最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常—这一点与迭代器协议一致。

这一班 Sentence 类比前一版简短多了,但是还不够懒惰。如今,人们认为惰性是好的特质,至少在编程语言和 API 中是如此。惰性实现是指尽可能延后生成值。这样做能节省内存,而且或许还可以避免做无用的处理。

下一节以这种惰性方式定义 Sentence 类。

Sentence 类第4版:惰性实现

re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个生成器,按需生成 re.MatchObject.

import re
import reprlibRE_WOR = re.compile('\w+')class Sentence:def __init__(self, text):self.text = textdef __repr__(self):return 'Sentence(%s)' % reprlib.repr(self.text)def __iter__(self):for match in RE_WOR.finditer(self.text):yield match.group()

生成器函数已经极大地简化了代码,但是使用生成器表达式甚至能把代码变得更简短。

Sentence类第5版:生成器表达式

生成器表达式可以理解为列表推导的惰性版本,不会迫切地构建列表,而是返回一个生成器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂。

import re
import reprlibRE_WOR = re.compile('\w+')class Sentence:def __init__(self, text):self.text = textdef __repr__(self):return 'Sentence(%s)' % reprlib.repr(self.text)def __iter__(self):return (match.group() for match in RE_WOR.finditer(self.text))

与上面的示例唯一的区别是 __iter__ 方法,这里不是生成器函数了(没有 yield), 而是使用生成器表达式构建生成器,然后将其返回。不过,最终的效果一样:调用 __iter__ 方法会得到一个生成器对象。

生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利。下一节说明生成器表达式的用途。

何时使用生成器表达式

生成器表达式是创建生成器的简洁句法,这样无需先定义函数再调用。不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用。

如果生成器表达式分成多行写,那么我倾向于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用。

  • 句法提示:如果函数或构造方法只有一个参数,传入生成器表达式时不用谢一对调用函数的括号,再写一对括号围住生成器表达式,只写一对括号就行了。

    def __mul__(self, scalar):
    if isinstance(scalar, numbers.Real):return Vector(n * scalar for n in self)
    else:return NotImplemented
    

接下来将介绍标准库中的生成器函数

标准库中的生成器函数

第一组是用于过滤的生成器函数

模块 函数 说明
itertools compress(it,selector_it) 并行处理两个可迭代的对象;如果 selector_it 中的元素是真值,产出 it 中对应的元素
itertools dropwhile(predicate, it) 处理 it,跳过 predicate 的计算结果为真值的元素,然后产出剩下的各个元素(不再进一步检查)
内置 filter(predicate, it) 把 it 中的各个元素传给 predicate,如果 predicate(item) 返回真值,那么产出对应的元素;如果 predicate 是 None,那么只产出真值元素
itertools filterfalse(predicate, it) 与 filter 函数的作用类似,不过 predicate 的逻辑是相反的:predicate 返回假值时产出对应的元素
itertools islice(it, stop) 或 islice(it, start, stop, step=1) 产出 it 的切片,作用类似于 s[:stop] 或 s[start:stop:step], 不过 it 可以是任何可迭代的对象,而且这个函数实现的是惰性操作
itertools takewhile(predicate, it) predicate 返回真值时产出对应的元素,然后立即停止,不再继续检查
>>> def vowel(c):
...     return c.lower() in 'aeiou'
...
>>> list(filter(vowel, 'Aardvark'))
['A', 'a', 'a']
>>> import itertools
>>> list(itertools.filterfalse(vowle, 'Aardvark'))
Traceback (most recent call last):File "<stdin>", line 1, in <module>
NameError: name 'vowle' is not defined
>>> list(itertools.filterfalse(vowel, 'Aardvark'))
['r', 'd', 'v', 'r', 'k']
>>> list(itertools.dropwhile(vowel, 'Aardvark'))
['r', 'd', 'v', 'a', 'r', 'k']
>>> list(itertools.dropwhile(vowel, 'Aaaaaaa'))
[]
>>> list(itertools.dropwhile(vowel, 'Aaaaaaabaaaaa'))
['b', 'a', 'a', 'a', 'a', 'a']
>>> list(itertools.takewhile(vowel, 'Aaaaaaaavaaaaa'))
['A', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
>>> list(itertools.compress('Aardvark', (1,0,1,1,0,1)))
['A', 'r', 'd', 'a']
>>> list(itertools.islice('Aardvark', 4))
['A', 'a', 'r', 'd']
>>> list(itertools.islice('Aardvark', 4, 7))
['v', 'a', 'r']
>>> list(itertools.islice('Aardvark', 1, 7, 2))
['a', 'd', 'a']

下一组是用于映射的生成器函数:在输入的单个可迭代对象(map 和 starmap 函数处理多个迭代的对象) 中的各个元素上做计算,然后返回结果。如果输入来自多个可迭代的对象,第一个可迭代的对象到头后就停止输出。

模块 函数 说明
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
>>> sample = [4,3,5,3,4,5,6,4]
>>> import itertools
>>> list(itertools.accumulate(sample))
[4, 7, 12, 15, 19, 24, 30, 34]
>>> list(itertools.accumulate(sample, min))
[4, 3, 3, 3, 3, 3, 3, 3]
>>> list(itertools.accumulate(sample, max))
[4, 4, 5, 5, 5, 5, 6, 6]
>>> import operator
>>> list(itertools.accumulate(sample, operator.mul))
[4, 12, 60, 180, 720, 3600, 21600, 86400]>>> list(enumerate('abldsfs', 1))
[(1, 'a'), (2, 'b'), (3, 'l'), (4, 'd'), (5, 's'), (6, 'f'), (7, 's')]
>>> import operator
>>> list(map(operator.mul, range(11), range(11)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> import itertools
>>> list(itertools.starmap(operator.mul, enumerate('asdfsef', 1)))
['a', 'ss', 'ddd', 'ffff', 'sssss', 'eeeeee', 'fffffff']

接下来这一组是用于合并的生成器函数,这些函数都从输入的多个可迭代对象中产出元素。chainchain.from_iterable 按顺序(一个接一个)处理输入的可迭代对象,而 product, zipzip_longest 并行处理输入的各个可迭代对象。

模块 函数 说明
itertools chain(it1,…,itN) 先产出 it1 中的所有元素,然后产出 it2 中的所有元素,以此类推,无缝连接在一起
itertools chain.from_iterable(it) 产出 it 生成的各个可迭代对象中的元素,一个接一个,无缝连接在一起;it 应该产出可迭代的元素,例如可迭代的对象列表
itertools product(it1, …, itN, repeat=1) 计算笛卡尔积:从输入的各个可迭代对象中获取元素,合并成由 N 个元素组成的元组,与嵌套的 for 循环效果一样;repeat 指明重复处理多少次输入的可迭代对象
(内置) zip(it1,…, itN) 并行从输入的各个可迭代对象中获取元素,产出由N个元素组成的元组,只要有一个可迭代的对象到头了,就默默地停止
itertools zip_longest(it1, …, itN, fillvalue=None) 并行从输入的各个可迭代对象中获取元素,产出由 N 个元素组成的元组,等到最长的可迭代对象到头后才停止,空缺的值使用 fillvalue填充
>>> import itertools
>>> list(itertools.chain('ABC', range(2)))
['A', 'B', 'C', 0, 1]
>>> list(itertools.chain(enumerate('ABC')))
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> list(itertools.chain.from_iterable(enumerate('ABC')))
[0, 'A', 1, 'B', 2, 'C']
>>> list(zip('ABC', range(5)))
[('A', 0), ('B', 1), ('C', 2)]
>>> list(itertools.zip_longest('ABC', range(5)))
[('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)]
>>> list(itertools.product('ABC', range(2)))
[('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]

有些生成器函数会从一个元素中产出多个值,扩展输入的可迭代对象

把输入的各个元素扩展成多个输出元素的生成器函数

模块 函数 说明
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))
itertools repeat(item, [times]) 重复不断地产出指定的元素,除非提供 times,指定次数
>>> ct = itertools.count()
>>> next(ct)
0
>>> next(ct)
1
>>> next(ct), next(ct)
(2, 3)
>>> cy = itertools.cycle('ABC')
>>> list(itertools.islice(cy, 7))
['A', 'B', 'C', 'A', 'B', 'C', 'A']
>>> rp = itertools.repeat(7)
>>> next(rp)
7
>>> next(rp)
7
>>> list(itertools.combinations('ABC', 2))
[('A', 'B'), ('A', 'C'), ('B', 'C')]
>>> list(itertools.combinations_with_replacement('ABC', 2))
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
>>> list(itertools.permutations('ABC', 2))
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
>>> list(itertools.product('ABC', repeat=2))
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]

最后一组生成器函数用于产出输入的可迭代对象中的全部元素,不过会以某种方式重新排列。

模块 函数 说明
itertools groupby(it, key=None) 产出由两个元素组成的元素,形式为(key, group),其中 key 是分组标准,group 是生成器,用于产出分组里的元素
(内置) reversed(seq) 从后向前,倒叙产出 seq 中的元素;seq 必须是序列,或者是实现了 reversed 特殊方法的对象
itertools tee(it, n=2) 产出一个由 n 个生成器组成的元组,每个生成器用于单独产出输入的可迭代对象中的元素
>>> list(itertools.groupby('LLLLAAGGG'))
[('L', <itertools._grouper object at 0x7f77fad53e80>), ('A', <itertools._grouper object at 0x7f77fad53b38>), ('G', <itertools._grouper object at 0x7f77facf52b0>)]
>>> for char, group in itertools.groupby('LLLLAAGG'):
...     print(char, '->', list(group))
...
L -> ['L', 'L', 'L', 'L']
A -> ['A', 'A']
G -> ['G', 'G']
>>> animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear', 'bat', 'dolphin', 'shark', 'lion']
>>> for length, group in itertools.groupby(animals, len):
...     print(length, '->', list(group))
...
4 -> ['duck']
5 -> ['eagle']
3 -> ['rat']
7 -> ['giraffe']
4 -> ['bear']
3 -> ['bat']
7 -> ['dolphin']
5 -> ['shark']
4 -> ['lion']
>>> animals.sort(key=len)
>>> for length, group in itertools.groupby(animals, len):
...     print(length, '->', list(group))
...
3 -> ['rat', 'bat']
4 -> ['duck', 'bear', 'lion']
5 -> ['eagle', 'shark']
7 -> ['giraffe', 'dolphin']>>> list(itertools.tee('ABC'))
[<itertools._tee object at 0x7f77f7393e08>, <itertools._tee object at 0x7f77f7393d88>]
>>> g1, g2 =list(itertools.tee('ABC'))
>>> next(g1)
'A'
>>> next(g2)
'A'
>>> list(g1)
['B', 'C']
>>> list(g2)
['B', 'C']

Python3.3中新出现的句法:yield from

如果生成器函数需要产出另一个生成器的值,传统的解决方法是使用嵌套的 for 循环。

>>> 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]

可以看出,yield from 完全代替了内层的 for 循环,在这个示例中使用 yield from 是对的,而且代码读起来更顺畅,不过感觉更像是语法糖。除了代替循环之外,yield from 还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。把生成器当成协程使用时,这个通道特别重要,不仅能为客户端代码生成值,还能使用客户端代码提供的值。

可迭代的归约函数

归约函数是指接受一个可迭代的对象,然后返回单个结果。这里列出的每个内置函数都可以使用 functools.reduce 函数实现,内置是因为使用它们便于解决常见的问题。此外,对 allany 函数来说,有一项重要的优化措施是 reduce 函数做不到的:这两个函数会短路(即一旦确定了结果就立即停止使用迭代器)。

模块 函数 说明
(内置) 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 函数提高精度)

深入分析 iter 函数

iter 函数还有一个鲜为人知的用法:传入两个参数,第一个参数必须是可调用的对象,用于不断调用,产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不铲除哨符。

>>> from random import randint
>>> def d6():
...     return randint(1, 6)
...
>>> d6_iter = iter(d6, 1)
>>> d6_iter
<callable_iterator object at 0x7f77f738e898>
>>> for roll in d6_iter:
...     print(roll)
...
4