返回介绍

14.16 延伸阅读

发布于 2024-02-05 21:59:47 字数 6614 浏览 0 评论 0 收藏 0

在 Python 语言参考手册中,“6.2.9. Yield expressions”从技术层面深入说明了生成器。定义生成器函数的 PEP 是“PEP 255—Simple Generators”。

itertools 模块的文档写得很棒,包含大量示例。虽然那个模块里的函数是使用 C 语言实现的,不过文档展示了如何使用 Python 实现部分函数,这通常要利用模块里的其他函数。用法示例也很好,例如,有一个代码片段说明如何使用 accumulate 函数计算带利息的分期还款,得出每次要还多少。文档中还有一节是“Itertools Recipes”,说明如何使用 itertools 模块中的现有函数实现额外的高性能函数。

在 David Beazley 与 Brian K. Jones 的《Python Cookbook(第 3 版)中文版》一书中,第 4 章有 16 个诀窍涵盖了这个话题,虽然角度不同,但都关注实际应用。

“What's New in Python 3.3”(参见“PEP 380: Syntax for Delegating to a Subgenerator”)通过示例说明了 yield from 句法。本书 16.7 节和 16.8 节还会讨论这个句法。

如果你对文档数据库感兴趣,想进一步了解 14.13 节的背景,可以阅读我发布在 Code4Lib Journal(涵盖图书馆与技术交集)上的论文,题为“From ISIS to CouchDB: Databases and Data Models for Bibliographic Records”,其中有一节对 isis2json.py 脚本做了说明。这篇论文的剩余内容说明文档数据库(如 CouchDB 和 MongoDB)实现半结构化数据模型的方式,以及为什么这种模型比关系模型更适合用于收集书目数据。

杂谈

生成器函数的语法糖多一些更好

在设计不同目的的控制和显示设备时,设计师需要确认它们之间具有明显差异。

——Donald Norman
《设计心理学》

在编程语言中,源码是“控制和显示设备”。我觉得 Python 设计得特别好,源码的可读性通常很高,好像伪代码一样。可是,没有什么是完美的。Guido van Rossum 应该遵从 Donald Norman 的建议(如上述引文),引入新的关键字,用于定义生成器函数,而不该继续使用 def。其实,“PEP 255 — Simple Generators”中的“BDFL Pronouncements”一节已经提议:

深藏于定义体中的“yield”语句不足以提醒语义发生了重大变化。

可是,Guido 讨厌引入新关键字,而且觉得这项提议没有说服力,因此我们只好被迫接受 def。

沿用函数句法定义生成器会导致几个不好的后果。在 Politz 等人发布的试验成果论文“Python, the Full Monty: A Tested Semantics for the Python Programming Language”18 中,有个简单的生成器函数示例(这篇论文的 4.1 节):

  def f(): x=0
    while True:
      x += 1
      yield x

然后,论文的作者指出,我们无法通过函数调用抽象产出这个过程(如示例 14-24 所示)。

示例 14-24 “(这样)似乎能简单地抽象产出这个过程”(Politz 等人)

def f():
  def do_yield(n):
    yield n
  x = 0
  while True:
    x += 1
    do_yield(x)

如果调用示例 14-24 中的 f(),会得到一个无限循环,而不是生成器,因为 yield 关键字只能把最近的外层函数变成生成器函数。虽然生成器函数看起来像函数,可是我们不能通过简单的函数调用把职责委托给另一个生成器函数。与此相比,Lua 语言就没有强加这一限制。在 Lua 中,协程可以调用其他函数,而且其中任何一个函数都能把职责交给原来的调用方。

Python 新引入的 yield from 句法允许生成器或协程把工作委托给第三方完成,这样就无需嵌套 for 循环作为变通了。在函数调用前面加上 yield from 能“解决”示例 14-24 中的问题,如示例 14-25 所示。

示例 14-25 这样才能简单地抽象产出这个过程

def f():
  def do_yield(n):
    yield n
  x = 0
  while True:
    x += 1
    yield from do_yield(x)

沿用 def 声明生成器犯了可用性方面的错误,而 Python 2.5 引入的协程(也写成包含 yield 关键字的函数)把这个问题进一步恶化了。在协程中,yield 碰巧(通常)出现在赋值语句的右手边,因为 yield 用于接收客户传给 .send() 方法的参数。正如 David Beazley 所说的:

尽管有一些相同之处,但是生成器和协程基本上是两个不同的概念。19

我觉得协程也应该有专用的关键字。读到后文你会发现,协程经常会用到特殊的装饰器,这样就能与其他的函数区分开。可是,生成器函数不常使用装饰器,因此我们不得不扫描函数的定义体,看有没有 yield 关键字,以此判断它究竟是普通的函数,还是完全不同的洪水猛兽。

也许有人会说,这么做是为了在不增加句法的前提下支持这些特性,即便添加额外的句法,也只是“语法糖”。可是,如果能让不同的特性看起来也不同,那么我更喜欢语法糖。Lisp 代码难以阅读的主要原因就是缺少语法糖,这也导致 Lisp 语言中的所有结构看起来都像是函数调用。

生成器与迭代器的语义对比

思考迭代器与生成器之间的关系时,至少可以从三方面入手。

第一方面是接口。Python 的迭代器协议定义了两个方法:__next__ 和 __iter__。生成器对象实现了这两个方法,因此从这方面来看,所有生成器都是迭代器。由此可以得知,内置的 enumerate() 函数创建的对象是迭代器:

  >>> from collections import abc
  >>> e = enumerate('ABC')
  >>> isinstance(e, abc.Iterator)
  True

第二方面是实现方式。从这个角度来看,生成器这种 Python 语言结构可以使用两种方式编写:含有 yield 关键字的函数,或者生成器表达式。调用生成器函数或者执行生成器表达式得到的生成器对象属于语言内部的 GeneratorType 类型(https://docs.python.org/3/library/types.html#types.GeneratorType)。从这方面来看,所有生成器都是迭代器,因为 GeneratorType 类型的实例实现了迭代器接口。不过,我们可以编写不是生成器的迭代器,方法是实现经典的迭代器模式,如示例 14-4 所示,或者使用 C 语言编写扩展。从这方面来看,enumerate 对象不是生成器:

  >>> import types
  >>> e = enumerate('ABC')
  >>> isinstance(e, types.GeneratorType)
  False

这是因为 types.GeneratorType 类型(https://docs.python.org/3/library/types.html#types.GeneratorType)是这样定义的:“生成器-迭代器对象的类型,调用生成器函数时生成。”

第三方面是概念。根据《设计模式:可复用面向对象软件的基础》一书的定义,在典型的迭代器设计模式中,迭代器用于遍历集合,从中产出元素。迭代器可能相当复杂,例如,遍历树状数据结构。但是,不管典型的迭代器中有多少逻辑,都是从现有的数据源中读取值;而且,调用 next(it) 时,迭代器不能修改从数据源中读取的值,只能原封不动地产出值。

而生成器可能无需遍历集合就能生成值,例如 range 函数。即便依附了集合,生成器不仅能产出集合中的元素,还可能会产出派生自元素的其他值。enumerate 函数是很好的例子。根据迭代器设计模式的原始定义,enumerate 函数返回的生成器不是迭代器,因为创建的是生成器产出的元组。

从概念方面来看,实现方式无关紧要。不使用 Python 生成器对象也能编写生成器。为了表明这一点,我写了一个斐波纳契数列生成器,如示例 14-26 所示。

示例 14-26 fibo_by_hand.py:不使用 GeneratorType 实例实现斐波纳契数列生成器

class Fibonacci:
    
  def __iter__(self):
    return FibonacciGenerator()
    
    
class FibonacciGenerator:
    
  def __init__(self):
    self.a = 0
    self.b = 1
    
  def __next__(self):
    result = self.a
    self.a, self.b = self.b, self.a + self.b
    return result

示例 14-26 虽然可行,但只是一个愚蠢的示例。符合 Python 风格的斐波纳契数列生成器如下所示:

  def fibonacci():
    a, b = 0, 1
    while True:
      yield a
      a, b = b, a + b

当然,始终可以使用生成器这个语言结构履行迭代器的基本职责:遍历集合,并从中产出元素。

事实上,Python 程序员不会严格区分二者,即便在官方文档中也把生成器称作迭代器。Python 词汇表对迭代器下的权威定义比较笼统,涵盖了迭代器和生成器。

迭代器:表示数据流的对象……

建议你读一下 Python 词汇表中对迭代器完整定义。而在生成器定义中,迭代器生成器是同义词,“生成器”指代生成器函数,以及生成器函数构建的生成器对象。因此,在 Python 社区的行话中,迭代器和生成器在一定程度上是同义词。

Python 中最简的迭代器接口

《设计模式:可复用面向对象软件的基础》一书讲解迭代器模式时,在“实现”一节中说道:20

迭代器的最小接口由 First、Next、IsDone 和 CurrentItem 操作组成。

不过,这句话有个脚注:

甚至可以将 Next、IsDone 和 CurrentItem 并入到一个操作中,该操作前进到下一个对象并返回这个对象,如果遍历结束,那么这个操作返回一个特定的值(例如,0)标志该迭代结束。这样我们就使这个接口变得更小了。

这与 Python 的做法接近:只用一个 __next__ 方法完成这项工作。不过,为了表明迭代结束,这个方法没有使用哨符,因为哨符可能不小心被忽略,而是使用 StopIteration 异常。简单且正确,这正是 Python 之道。

18Joe Gibbs Politz, Alejandro Martinez, Matthew Milano, Sumner Warren, Daniel Patterson, Junsong Li, Anand Chitipothu, and Shriram Krishnamurthi,“Python: The Full Monty,”SIGPLAN Not. 48, 10 (October 2013), 217-232.

19A Curious Course on Coroutines and Concurrency”,第 31 张幻灯片。

20《设计模式:可复用面向对象软件的基础》第 174 页。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文