流畅的 Python 第二版(GPT 重译)(九)(3)

简介: 流畅的 Python 第二版(GPT 重译)(九)

流畅的 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 * 26)。

两个卡片等级('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_lenlen(list(it))
itertools repeat(item, [times]) 重复产生给定的项目,除非给定了times次数
^(a) itertools.pairwise在 Python 3.10 中添加。

itertools中的countrepeat函数返回生成器,从无中生有地产生项目:它们都不接受可迭代对象作为输入。我们在“使用 itertools 进行算术进度”中看到了itertools.countcycle生成器备份输入可迭代对象并重复产生其项目。示例 17-20 演示了countcyclepairwiserepeat的用法。

示例 17-20。countcyclepairwiserepeat
>>> 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生成器由islicetakewhile限制,我可以构建一个list

从’ABC’构建一个cycle生成器,并获取其第一个项目,‘A’。

只有通过islice限制,才能构建一个list;这里检索了接下来的七个项目。

对于输入中的每个项目,pairwise产生一个包含该项目和下一个项目(如果有下一个项目)的 2 元组。在 Python ≥ 3.10 中可用。

构建一个repeat生成器,永远产生数字7

通过传递times参数,可以限制repeat生成器:这里数字8将产生4次。

repeat的常见用法:在map中提供一个固定参数;这里提供了5的倍数。

combinationscombinations_with_replacementpermutations生成器函数——连同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.groupbyitertools.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.groupbyreversed内置函数的使用。请注意,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进行排序。

再次循环遍历keygroup对,以显示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:哈希和更快的==”中。

allany的情况下,有一个重要的优化functools.reduce不支持:allany短路——即,它们在确定结果后立即停止消耗迭代器。请参见示例 17-24 中any的最后一个测试。

表 17-6. 读取可迭代对象并返回单个值的内置函数

模块 函数 描述
(内置) all(it) 如果it中所有项目都为真,则返回True,否则返回Falseall([])返回True
(内置) any(it) 如果it中有任何项目为真,则返回True,否则返回Falseany([])返回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=?]),在这种情况下,返回参数中的最小值。

allany的操作在示例 17-24 中有所体现。

示例 17-24. 对一些序列使用allany的结果
>>> 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

anyg上迭代直到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会暂停gensub_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

相关文章
|
13天前
|
存储 IDE JavaScript
流畅的 Python 第二版(GPT 重译)(四)(2)
流畅的 Python 第二版(GPT 重译)(四)
40 1
|
13天前
|
存储 Python
流畅的 Python 第二版(GPT 重译)(十)(2)
流畅的 Python 第二版(GPT 重译)(十)
21 0
|
13天前
|
机器学习/深度学习 Serverless Python
流畅的 Python 第二版(GPT 重译)(六)(4)
流畅的 Python 第二版(GPT 重译)(六)
12 1
|
Linux 数据库 iOS开发
流畅的 Python 第二版(GPT 重译)(二)(4)
流畅的 Python 第二版(GPT 重译)(二)
48 5
|
13天前
|
JavaScript 安全 前端开发
流畅的 Python 第二版(GPT 重译)(六)(1)
流畅的 Python 第二版(GPT 重译)(六)
62 1
|
13天前
|
存储 安全 测试技术
流畅的 Python 第二版(GPT 重译)(四)(3)
流畅的 Python 第二版(GPT 重译)(四)
6 1
|
13天前
|
存储 设计模式 缓存
流畅的 Python 第二版(GPT 重译)(五)(2)
流畅的 Python 第二版(GPT 重译)(五)
29 1
|
13天前
|
存储 API 芯片
流畅的 Python 第二版(GPT 重译)(九)(2)
流畅的 Python 第二版(GPT 重译)(九)
63 1
|
13天前
|
JSON JavaScript Java
流畅的 Python 第二版(GPT 重译)(三)(3)
流畅的 Python 第二版(GPT 重译)(三)
45 5
|
13天前
|
缓存 算法 Java
流畅的 Python 第二版(GPT 重译)(三)(4)
流畅的 Python 第二版(GPT 重译)(三)
43 4