Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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%精确的语义和语法规范。)

  1. 生成器表达式的语义等同于创建一个匿名生成器函数并调用它。例如
    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
    
  2. 语法要求生成器表达式必须直接位于一组括号内,并且两边都不能有逗号。参照 CVS 中 Grammar/Grammar 文件,有两条规则发生变化:
    1. 规则
      atom: '(' [testlist] ')'
      

      变为

      atom: '(' [testlist_gexp] ')'
      

      其中 testlist_gexp 几乎与 listmaker 相同,但 'for' ... 'in' 之后只允许一个测试。

      testlist_gexp: test ( gen_for | (',' test)* [','] )
      
    2. 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中确认。

  3. 循环变量(如果它是一个简单变量或一个简单变量的元组)不会暴露给周围的函数。这有助于实现并使典型用例更可靠。在未来某个版本的 Python 中,列表推导式也将对周围的代码隐藏归纳变量(并且,在 Py2.4 中,访问归纳变量的代码将发出警告)。

    例如

    x = "hello"
    y = list(x for x in "abc")
    print x    # prints "hello", not "c"
    
  4. 列表推导式将保持不变。例如
    [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

最后修改: 2025-02-01 08:55:40 GMT