流畅的 Python 第二版(GPT 重译)(九)(2)https://developer.aliyun.com/article/1484705
示例 17-19. itertools.product
生成器函数示例
>>> list(itertools.product('ABC', range(2))) # ① [('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)] >>> suits = 'spades hearts diamonds clubs'.split() >>> list(itertools.product('AK', suits)) # ② [('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'), ('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')] >>> list(itertools.product('ABC')) # ③ [('A',), ('B',), ('C',)] >>> list(itertools.product('ABC', repeat=2)) # ④ [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')] >>> list(itertools.product(range(2), repeat=3)) [(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)] >>> rows = itertools.product('AB', range(2), repeat=2) >>> for row in rows: print(row) ... ('A', 0, 'A', 0) ('A', 0, 'A', 1) ('A', 0, 'B', 0) ('A', 0, 'B', 1) ('A', 1, 'A', 0) ('A', 1, 'A', 1) ('A', 1, 'B', 0) ('A', 1, 'B', 1) ('B', 0, 'A', 0) ('B', 0, 'A', 1) ('B', 0, 'B', 0) ('B', 0, 'B', 1) ('B', 1, 'A', 0) ('B', 1, 'A', 1) ('B', 1, 'B', 0) ('B', 1, 'B', 1)
①
一个具有三个字符的str
和一个具有两个整数的range
的笛卡尔积产生六个元组(因为3 * 2
是6
)。
②
两个卡片等级('AK'
)和四个花色的乘积是一系列八元组。
③
给定一个单个可迭代对象,product
生成一系列单元组,不是很有用。
④
repeat=N
关键字参数告诉产品消耗每个输入可迭代对象N
次。
一些生成器函数通过产生每个输入项多个值来扩展输入。它们在表 17-4 中列出。
表 17-4. 将每个输入项扩展为多个输出项的生成器函数
模块 | 函数 | 描述 |
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 |
pairwise(it) |
从输入可迭代对象中获取连续的重叠对^(a) |
itertools |
permutations(it, out_len=None) |
从it 产生的项目中产生out_len 个项目的排列;默认情况下,out_len 为len(list(it)) |
itertools |
repeat(item, [times]) |
重复产生给定的项目,除非给定了times 次数 |
^(a) itertools.pairwise 在 Python 3.10 中添加。 |
itertools
中的count
和repeat
函数返回生成器,从无中生有地产生项目:它们都不接受可迭代对象作为输入。我们在“使用 itertools 进行算术进度”中看到了itertools.count
。cycle
生成器备份输入可迭代对象并重复产生其项目。示例 17-20 演示了count
、cycle
、pairwise
和repeat
的用法。
示例 17-20。count
、cycle
、pairwise
和repeat
>>> ct = itertools.count() # ① >>> next(ct) # ② 0 >>> next(ct), next(ct), next(ct) # ③ (1, 2, 3) >>> list(itertools.islice(itertools.count(1, .3), 3)) # ④ [1, 1.3, 1.6] >>> cy = itertools.cycle('ABC') # ⑤ >>> next(cy) 'A' >>> list(itertools.islice(cy, 7)) # ⑥ ['B', 'C', 'A', 'B', 'C', 'A', 'B'] >>> list(itertools.pairwise(range(7))) # ⑦ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)] >>> rp = itertools.repeat(7) # ⑧ >>> next(rp), next(rp) (7, 7) >>> list(itertools.repeat(8, 4)) # ⑨ [8, 8, 8, 8] >>> list(map(operator.mul, range(11), itertools.repeat(5))) # ⑩ [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
①
构建一个count
生成器ct
。
②
从ct
中检索第一个项目。
③
我无法从ct
中构建一个list
,因为ct
永远不会停止,所以我获取了接下来的三个项目。
④
如果count
生成器由islice
或takewhile
限制,我可以构建一个list
。
⑤
从’ABC’构建一个cycle
生成器,并获取其第一个项目,‘A’。
⑥
只有通过islice
限制,才能构建一个list
;这里检索了接下来的七个项目。
⑦
对于输入中的每个项目,pairwise
产生一个包含该项目和下一个项目(如果有下一个项目)的 2 元组。在 Python ≥ 3.10 中可用。
⑧
构建一个repeat
生成器,永远产生数字7
。
⑨
通过传递times
参数,可以限制repeat
生成器:这里数字8
将产生4
次。
⑩
repeat
的常见用法:在map
中提供一个固定参数;这里提供了5
的倍数。
combinations
、combinations_with_replacement
和permutations
生成器函数——连同product
——在itertools
文档页面中被称为组合生成器。itertools.product
与其余组合函数之间也有密切关系,正如示例 17-21 所示。
示例 17-21。组合生成器函数从每个输入项目中产生多个值
>>> 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')]
①
从’ABC’中的项目中生成len()==2
的所有组合;生成的元组中的项目顺序无关紧要(它们可以是集合)。
②
从’ABC’中的项目中生成len()==2
的所有组合,包括重复项目的组合。
③
从’ABC’中的项目中生成len()==2
的所有排列;生成的元组中的项目顺序是相关的。
④
从’ABC’和’ABC’中的笛卡尔积(这是repeat=2
的效果)。
我们将在本节中介绍的最后一组生成器函数旨在以某种方式重新排列输入可迭代对象中的所有项目。以下是返回多个生成器的两个函数:itertools.groupby
和itertools.tee
。该组中的另一个生成器函数,reversed
内置函数,是本节中唯一一个不接受任何可迭代对象作为输入的函数,而只接受序列。这是有道理的:因为reversed
将从最后到第一个产生项目,所以它只能与已知长度的序列一起使用。但它通过根据需要产生每个项目来避免制作反转副本的成本。我将itertools.product
函数与表 17-3 中的合并生成器放在一起,因为它们都消耗多个可迭代对象,而表 17-5 中的生成器最多只接受一个输入可迭代对象。
表 17-5. 重新排列生成器函数
模块 | 函数 | 描述 |
itertools |
groupby(it, key=None) |
产生形式为(key, group) 的 2 元组,其中key 是分组标准,group 是产生组中项目的生成器 |
(内置) | reversed(seq) |
以从最后到第一个的顺序从seq 中产生项目;seq 必须是一个序列或实现__reversed__ 特殊方法 |
itertools |
tee(it, n=2) |
产生一个元组,其中包含n个独立产生输入可迭代对象的项目的生成器 |
示例 17-22 演示了itertools.groupby
和reversed
内置函数的使用。请注意,itertools.groupby
假定输入可迭代对象按分组标准排序,或者至少按照该标准对项目进行了分组,即使不完全排序。技术审阅者 Miroslav Šedivý建议了这种用例:您可以按时间顺序对datetime
对象进行排序,然后按星期几进行分组,以获取星期一数据组,接着是星期二数据组,依此类推,然后再次是下周的星期一数据组,依此类推。
示例 17-22. itertools.groupby
>>> list(itertools.groupby('LLLLAAGGG')) # ① [('L', <itertools._grouper object at 0x102227cc0>), ('A', <itertools._grouper object at 0x102227b38>), ('G', <itertools._grouper object at 0x102227b70>)] >>> for char, group in itertools.groupby('LLLLAAAGG'): # ② ... print(char, '->', list(group)) ... L -> ['L', 'L', 'L', 'L'] A -> ['A', 'A',] G -> ['G', 'G', 'G'] >>> animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear', ... 'bat', 'dolphin', 'shark', 'lion'] >>> animals.sort(key=len) # ③ >>> animals ['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark', 'giraffe', 'dolphin'] >>> 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'] >>> for length, group in itertools.groupby(reversed(animals), len): # ⑤ ... print(length, '->', list(group)) ... 7 -> ['dolphin', 'giraffe'] 5 -> ['shark', 'eagle'] 4 -> ['lion', 'bear', 'duck'] 3 -> ['bat', 'rat'] >>>
①
groupby
产生(key, group_generator)
的元组。
②
处理groupby
生成器涉及嵌套迭代:在这种情况下,外部for
循环和内部list
构造函数。
③
按长度对animals
进行排序。
④
再次循环遍历key
和group
对,以显示key
并将group
扩展为list
。
⑤
这里reverse
生成器从右到左迭代animals
。
该组中最后一个生成器函数是iterator.tee
,具有独特的行为:它从单个输入可迭代对象产生多个生成器,每个生成器都从输入中产生每个项目。这些生成器可以独立消耗,如示例 17-23 所示。
示例 17-23. itertools.tee
生成多个生成器,每个生成器都生成输入生成器的每个项目
>>> list(itertools.tee('ABC')) [<itertools._tee object at 0x10222abc8>, <itertools._tee object at 0x10222ac08>] >>> g1, g2 = itertools.tee('ABC') >>> next(g1) 'A' >>> next(g2) 'A' >>> next(g2) 'B' >>> list(g1) ['B', 'C'] >>> list(g2) ['C'] >>> list(zip(*itertools.tee('ABC'))) [('A', 'A'), ('B', 'B'), ('C', 'C')]
请注意,本节中的几个示例使用了生成器函数的组合。这些函数的一个很好的特性是:因为它们接受生成器作为参数并返回生成器,所以它们可以以许多不同的方式组合在一起。
现在我们将回顾标准库中另一组对可迭代对象敏感的函数。
可迭代对象减少函数
表 17-6 中的函数都接受一个可迭代对象并返回一个单一结果。它们被称为“reducing”、“folding”或“accumulating”函数。我们可以使用functools.reduce
实现这里列出的每一个内置函数,但它们作为内置函数存在是因为它们更容易地解决了一些常见的用例。有关functools.reduce
的更长解释出现在“向量取#4:哈希和更快的==”中。
在all
和any
的情况下,有一个重要的优化functools.reduce
不支持:all
和any
短路——即,它们在确定结果后立即停止消耗迭代器。请参见示例 17-24 中any
的最后一个测试。
表 17-6. 读取可迭代对象并返回单个值的内置函数
模块 | 函数 | 描述 |
(内置) | all(it) |
如果it 中所有项目都为真,则返回True ,否则返回False ;all([]) 返回True |
(内置) | any(it) |
如果it 中有任何项目为真,则返回True ,否则返回False ;any([]) 返回False |
(内置) | max(it, [key=,] [default=]) |
返回it 中项目的最大值;^(a) key 是一个排序函数,就像sorted 中一样;如果可迭代对象为空,则返回default |
(内置) | min(it, [key=,] [default=]) |
返回it 中项目的最小值。^(b) key 是一个排序函数,就像sorted 中一样;如果可迭代对象为空,则返回default |
functools |
reduce(func, it, [initial]) |
返回将func 应用于第一对项目的结果,然后应用于该结果和第三个项目,依此类推;如果给定,initial 将与第一个项目形成初始对 |
(内置) | sum(it, start=0) |
it 中所有项目的总和,加上可选的start 值(在添加浮点数时使用math.fsum 以获得更好的精度) |
^(a) 也可以称为max(arg1, arg2, …, [key=?]) ,在这种情况下,返回参数中的最大值。^(b) 也可以称为min(arg1, arg2, …, [key=?]) ,在这种情况下,返回参数中的最小值。 |
all
和any
的操作在示例 17-24 中有所体现。
示例 17-24. 对一些序列使用all
和any
的结果
>>> all([1, 2, 3]) True >>> all([1, 0, 3]) False >>> all([]) True >>> any([1, 2, 3]) True >>> any([1, 0, 3]) True >>> any([0, 0.0]) False >>> any([]) False >>> g = (n for n in [0, 0.0, 7, 8]) >>> any(g) # ① True >>> next(g) # ② 8
①
any
在g
上迭代直到g
产生7
;然后any
停止并返回True
。
②
这就是为什么8
仍然保留着。
另一个接受可迭代对象并返回其他内容的内置函数是sorted
。与生成器函数reversed
不同,sorted
构建并返回一个新的list
。毕竟,必须读取输入可迭代对象的每个单个项目以便对它们进行排序,排序发生在一个list
中,因此sorted
在完成后只返回该list
。我在这里提到sorted
是因为它消耗任意可迭代对象。
当然,sorted
和减少函数只适用于最终会停止的可迭代对象。否则,它们将继续收集项目并永远不会返回结果。
注意
如果你已经看到了本章节最重要和最有用的内容,剩下的部分涵盖了大多数人不经常看到或需要的高级生成器功能,比如yield from
结构和经典协程。
还有关于类型提示可迭代对象、迭代器和经典协程的部分。
yield from
语法提供了一种组合生成器的新方法。接下来是这个。
使用yield from
的子生成器
yield from
表达式语法在 Python 3.3 中引入,允许生成器将工作委托给子生成器。
在引入yield from
之前,当生成器需要产生另一个生成器生成的值时,我们使用for
循环:
>>> def sub_gen(): ... yield 1.1 ... yield 1.2 ... >>> def gen(): ... yield 1 ... for i in sub_gen(): ... yield i ... yield 2 ... >>> for x in gen(): ... print(x) ... 1 1.1 1.2 2
我们可以使用yield from
得到相同的结果,就像你在示例 17-25 中看到的那样。
示例 17-25. 测试驱动yield from
>>> def sub_gen(): ... yield 1.1 ... yield 1.2 ... >>> def gen(): ... yield 1 ... yield from sub_gen() ... yield 2 ... >>> for x in gen(): ... print(x) ... 1 1.1 1.2 2
在示例 17-25 中,for
循环是客户端代码,gen
是委托生成器,sub_gen
是子生成器。请注意,yield from
会暂停gen
,sub_gen
接管直到耗尽。由sub_gen
生成的值直接通过gen
传递给客户端for
循环。同时,gen
被挂起,无法看到通过它传递的值。只有当sub_gen
完成时,gen
才会恢复。
当子生成器包含带有值的return
语句时,该值可以通过在表达式中使用yield from
在委托生成器中捕获。示例 17-26 演示了这一点。
示例 17-26. yield from
获取子生成器的返回值
>>> def sub_gen(): ... yield 1.1 ... yield 1.2 ... return 'Done!' ... >>> def gen(): ... yield 1 ... result = yield from sub_gen() ... print('<--', result) ... yield 2 ... >>> for x in gen(): ... print(x) ... 1 1.1 1.2 <-- Done! 2
现在我们已经了解了yield from
的基础知识,让我们研究一些简单但实用的用法示例。
重塑链
我们在表 17-3 中看到,itertools
提供了一个chain
生成器,从多个可迭代对象中产生项目,首先迭代第一个,然后迭代第二个,依此类推,直到最后一个。这是在 Python 中使用嵌套for
循环实现的chain
的自制版本:¹⁰
>>> def chain(*iterables): ... for it in iterables: ... for i in it: ... yield i ... >>> s = 'ABC' >>> r = range(3) >>> list(chain(s, r)) ['A', 'B', 'C', 0, 1, 2]
在前面的代码中,chain
生成器依次委托给每个可迭代对象it
,通过驱动内部for
循环中的每个it
。该内部循环可以用yield from
表达式替换,如下一个控制台列表所示:
>>> def chain(*iterables): ... for i in iterables: ... yield from i ... >>> list(chain(s, t)) ['A', 'B', 'C', 0, 1, 2]
在这个示例中使用yield from
是正确的,代码读起来更好,但似乎只是一点点语法糖而已。现在让我们开发一个更有趣的示例。
遍历树
在本节中,我们将看到在脚本中使用yield from
来遍历树结构。我将逐步构建它。
本示例的树结构是 Python 的异常层次结构。但是该模式可以适应显示目录树或任何其他树结构。
从 Python 3.10 开始,异常层次结构从零级的BaseException
开始,深达五级。我们的第一步是展示零级。
给定一个根类,在示例 17-27 中的tree
生成器会生成其名称并停止。
示例 17-27. tree/step0/tree.py:生成根类的名称并停止
def tree(cls): yield cls.__name__ def display(cls): for cls_name in tree(cls): print(cls_name) if __name__ == '__main__': display(BaseException)
示例 17-27 的输出只有一行:
BaseException • 1
下一个小步骤将我们带到第 1 级。tree
生成器将生成根类的名称和每个直接子类的名称。子类的名称缩进以显示层次结构。这是我们想要的输出:
$ python3 tree.py BaseException Exception GeneratorExit SystemExit KeyboardInterrupt
示例 17-28 产生了该输出。
示例 17-28. tree/step1/tree.py:生成根类和直接子类的名称
def tree(cls): yield cls.__name__, 0 # ① for sub_cls in cls.__subclasses__(): # ② yield sub_cls.__name__, 1 # ③ def display(cls): for cls_name, level in tree(cls): indent = ' ' * 4 * level # ④ print(f'{indent}{cls_name}') if __name__ == '__main__': display(BaseException)
①
为了支持缩进输出,生成类的名称和其在层次结构中的级别。
②
使用__subclasses__
特殊方法获取子类列表。
③
产出子类和第 1 级的名称。
④
构建缩进字符串,为 level
乘以 4
个空格。在零级时,这将是一个空字符串。
在 示例 17-29 中,我重构了 tree
,将根类的特殊情况与子类分开处理,现在在 sub_tree
生成器中处理。在 yield from
处,tree
生成器被挂起,sub_tree
接管产出值。
示例 17-29. tree/step2/tree.py: tree
产出根类名称,然后委托给 sub_tree
def tree(cls): yield cls.__name__, 0 yield from sub_tree(cls) # ① def sub_tree(cls): for sub_cls in cls.__subclasses__(): yield sub_cls.__name__, 1 # ② def display(cls): for cls_name, level in tree(cls): # ③ indent = ' ' * 4 * level print(f'{indent}{cls_name}') if __name__ == '__main__': display(BaseException)
①
委托给 sub_tree
产出子类的名称。
②
产出每个子类和第 1 级的名称。由于 tree
内部有 yield from sub_tree(cls)
,这些值完全绕过了 tree
生成器函数…
③
… 并直接在这里接收。
为了进行 深度优先 树遍历,在产出第 1 级的每个节点后,我想要产出该节点的第 2 级子节点,然后继续第 1 级。一个嵌套的 for
循环负责处理这个问题,就像 示例 17-30 中一样。
示例 17-30. tree/step3/tree.py: sub_tree
深度优先遍历第 1 和第 2 级
def tree(cls): yield cls.__name__, 0 yield from sub_tree(cls) def sub_tree(cls): for sub_cls in cls.__subclasses__(): yield sub_cls.__name__, 1 for sub_sub_cls in sub_cls.__subclasses__(): yield sub_sub_cls.__name__, 2 def display(cls): for cls_name, level in tree(cls): indent = ' ' * 4 * level print(f'{indent}{cls_name}') if __name__ == '__main__': display(BaseException)
这是从 示例 17-30 运行 step3/tree.py 的结果:
$ python3 tree.py BaseException Exception TypeError StopAsyncIteration StopIteration ImportError OSError EOFError RuntimeError NameError AttributeError SyntaxError LookupError ValueError AssertionError ArithmeticError SystemError ReferenceError MemoryError BufferError Warning GeneratorExit SystemExit KeyboardInterrupt
你可能已经知道这将会发生什么,但我将再次坚持小步慢走:让我们通过添加另一个嵌套的 for
循环来达到第 3 级。程序的其余部分没有改变,因此 示例 17-31 仅显示了 sub_tree
生成器。
流畅的 Python 第二版(GPT 重译)(九)(4)https://developer.aliyun.com/article/1484708