PEP 289 – 生成器表达式
- 作者:
- Raymond Hettinger <python at rcn.com>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2002年1月30日
- Python 版本:
- 2.4
- 发布历史:
- 2003年10月22日
摘要
本 PEP 引入了生成器表达式,作为列表推导式 PEP 202 和生成器 PEP 255 的一种高性能、内存高效的泛化。
基本原理
列表推导式的使用经验表明了其在 Python 中的广泛实用性。但是,许多用例不需要在内存中创建完整的列表。相反,它们只需要逐个迭代元素。
例如,以下求和代码将在内存中构建一个完整的平方数列表,迭代这些值,并在不再需要该引用时删除该列表
sum([x*x for x in range(10)])
使用生成器表达式可以节省内存
sum(x*x for x in range(10))
容器对象的构造函数也获得了类似的好处
s = set(word for line in page for word in line.split())
d = dict( (k, func(k)) for k in keylist)
生成器表达式在像 sum()、min() 和 max() 这样的函数中特别有用,这些函数将可迭代输入归约为单个值
max(len(line) for line in file if line.strip())
生成器表达式也解决了使用 lambda 编写的某些函数式编程示例
reduce(lambda s, a: s + a.myattr, data, 0)
reduce(lambda s, a: s + a[3], data, 0)
这些可以简化为
sum(a.myattr for a in data)
sum(a[3] for a in data)
列表推导式极大地减少了对 filter() 和 map() 的需求。同样,生成器表达式预计将最大程度地减少对 itertools.ifilter() 和 itertools.imap() 的需求。相反,其他 itertools 的实用性将通过生成器表达式得到增强
dotproduct = sum(x*y for x,y in itertools.izip(x_vector, y_vector))
具有类似于列表推导式的语法也使得在扩展应用程序时很容易将现有代码转换为生成器表达式。
早期的计时结果表明,生成器相对于列表推导式具有显著的性能优势。但是,后者针对 Py2.4 进行了高度优化,现在对于小型到中型数据集而言,性能大致相当。随着数据量的增加,生成器表达式的性能往往更好,因为它们不会耗尽缓存内存,并且允许 Python 在迭代之间重用对象。
BDFL 声明
此 PEP 已被 Py2.4 接受。
细节
(对于来自火星的读者来说,这些内容都不够精确,但我希望这些示例能够很好地传达意图,以便在 c.l.py 中进行讨论。Python 参考手册应该包含 100% 精确的语义和语法规范。)
- 生成器表达式的语义等同于创建一个匿名生成器函数并调用它。例如
g = (x**2 for x in range(10)) print g.next()
等同于
def __gen(exp): for x in exp: yield x**2 g = __gen(iter(range(10))) print g.next()
只有最外层的 for 表达式会被立即求值,其他表达式会被延迟到生成器运行时再求值
g = (tgtexp for var1 in exp1 if exp2 for var2 in exp3 if exp4)
等同于
def __gen(bound_exp): for var1 in bound_exp: if exp2: for var2 in exp3: if exp4: yield tgtexp g = __gen(iter(exp1)) del __gen
- 语法要求生成器表达式始终需要直接位于一组括号内,并且两侧不能有逗号。参考 CVS 中的 Grammar/Grammar 文件,有两个规则发生了变化
- 规则
atom: '(' [testlist] ')'
更改为
atom: '(' [testlist_gexp] ')'
其中 testlist_gexp 几乎与 listmaker 相同,但只允许在 ‘for’ … ‘in’ 之后使用单个 test
testlist_gexp: test ( gen_for | (',' test)* [','] )
- arglist 的规则需要类似的更改。
这意味着你可以写
sum(x**2 for x in range(10))
但你必须写
reduce(operator.add, (x**2 for x in range(10)))
以及
g = (x**2 for x in range(10))
即,如果函数调用只有一个位置参数,它可以是没有任何额外括号的生成器表达式,但在所有其他情况下,你必须用括号括起来。
确切的细节已签入 Grammar/Grammar 版本 1.49 中。
- 规则
- 循环变量(如果它是简单变量或简单变量的元组)不会暴露给周围的函数。这有助于实现,并使典型用例更加可靠。在 Python 的某个未来版本中,列表推导式也将隐藏感应变量,使其不被周围的代码访问(并且,在 Py2.4 中,将针对访问感应变量的代码发出警告)。
例如
x = "hello" y = list(x for x in "abc") print x # prints "hello", not "c"
- 列表推导式将保持不变。例如
[x for x in S] # This is a list comprehension. [(x for x in S)] # This is a list containing one generator # expression.
不幸的是,目前存在细微的语法差异。表达式
[x for x in 1, 2, 3]
是合法的,表示
[x for x in (1, 2, 3)]
但生成器表达式不允许使用前一种版本
(x for x in 1, 2, 3)
是非法的。
前一种列表推导式语法将在 Python 3.0 中变得非法,并且应该在 Python 2.4 及更高版本中弃用。
列表推导式还会将其循环变量“泄漏”到周围的作用域中。这在 Python 3.0 中也会发生变化,因此 Python 3.0 中列表推导式的语义定义将等同于 list(<生成器表达式>)。Python 2.4 及更高版本应该在列表推导式的循环变量与紧邻周围作用域中使用的变量同名时发出弃用警告。
早期绑定与后期绑定
经过多次讨论,最终决定第一个(最外层)for 表达式应该立即求值,而其余表达式应该在生成器执行时求值。
当被要求总结绑定第一个表达式的理由时,Guido 提供了 [1]
Consider sum(x for x in foo()). Now suppose there's a bug in foo()
that raises an exception, and a bug in sum() that raises an
exception before it starts iterating over its argument. Which
exception would you expect to see? I'd be surprised if the one in
sum() was raised rather the one in foo(), since the call to foo()
is part of the argument to sum(), and I expect arguments to be
processed before the function is called.
OTOH, in sum(bar(x) for x in foo()), where sum() and foo()
are bugfree, but bar() raises an exception, we have no choice but
to delay the call to bar() until sum() starts iterating -- that's
part of the contract of generators. (They do nothing until their
next() method is first called.)
有人提出了各种用例,建议在定义生成器时绑定所有自由变量。并且一些支持者认为,如果立即绑定,生成的表达式将更容易理解和调试。
但是,Python 对 lambda 表达式采用后期绑定方法,并且没有自动早期绑定的先例。人们认为引入新的范式会不必要地引入复杂性。
在探索了许多可能性之后,人们达成共识,即绑定问题很难理解,并且应该强烈鼓励用户在立即使用其参数的函数内部使用生成器表达式。对于更复杂的应用程序,完整的生成器定义在作用域、生命周期和绑定方面始终更优越 [2]。
归约函数
当与归约函数(如 sum()、min() 和 max())结合使用时,生成器表达式的实用性大大增强。Python 2.4 中的 heapq 模块包含两个新的归约函数:nlargest() 和 nsmallest()。两者都与生成器表达式配合良好,并且一次最多只在内存中保留 n 个项目。
致谢
- Raymond Hettinger 于 2002 年 1 月首次提出了“生成器推导式”的想法。
- Peter Norvig 在其关于累积显示的提案中重新引发了讨论。
- Alex Martelli 提供了关键的测量结果,证明了生成器表达式的性能优势。他还提供了强有力的论点,证明它们是值得拥有的东西。
- Phillip Eby 建议将“迭代器表达式”作为名称。
- 随后,Tim Peters 建议使用“生成器表达式”作为名称。
- Armin Rigo、Tim Peters、Guido van Rossum、Samuele Pedroni、Hye-Shik Chang 和 Raymond Hettinger 讨论了早期绑定与后期绑定相关的问题 [1]。
- Jiwon Seo 单独实现了该提案的各种版本,包括加载到 CVS 中的最终版本。在此过程中,Hye-Shik Chang 和 Raymond Hettinger 定期审查代码。Guido van Rossum 在 Armin Rigo 的评论和新闻组讨论之后做出了关键的设计决策。Raymond Hettinger 提供了测试套件、文档、教程和示例 [2]。
参考文献
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0289.rst
上次修改: 2023-09-09 17:39:29 GMT