PEP 3113 – 移除元组参数解包
- 作者:
- Brett Cannon <brett at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2007年3月2日
- Python 版本:
- 3.0
- 发布历史:
摘要
元组参数解包是指在函数签名中使用元组作为参数,以便自动解包序列参数。例如:
def fxn(a, (b, c), d):
pass
在签名中使用 (b, c) 要求函数的第二个参数是一个长度为二的序列(例如,[42, -13])。当这样的序列被传递时,它会被解包并将其值赋给参数,就像在参数中执行了语句 b, c = [42, -13] 一样。
不幸的是,Python 丰富函数签名功能中的这个特性,虽然在某些情况下很方便,但它带来的问题多于其价值。因此,本 PEP 提议在 Python 3.0 中从语言中移除它们。
为何应将其移除
内省问题
Python 具有非常强大的内省能力。这些能力延伸到函数签名。函数的调用签名没有任何隐藏的细节。通常,通过查看函数对象及其上的各种属性(包括函数的 func_code 属性),很容易找出函数签名的各种细节。
但在元组参数方面存在很大的困难。元组参数的存在是通过其名称由函数代码对象的 co_varnames 属性中的 . 和一个数字组成来表示的。这允许元组参数绑定到一个只有字节码才能知道且无法在 Python 源代码中输入的名称。但这并没有指定元组的格式:它的长度、是否存在嵌套元组等。
为了从函数中获取元组的所有细节,必须分析函数的字节码。这是因为函数中的第一个字节码实际上会将元组参数解包。假设元组参数名为 .1,并且预期解包为变量 spam 和 monty(意味着它是元组 (spam, monty)),则函数中的第一个字节码将用于语句 spam, monty = .1。这意味着要了解元组参数的所有细节,必须查看函数的初始字节码以检测格式为 \.\d+ 的参数的元组解包,并推断出关于预期参数的任何和所有信息。inspect.getargspec 函数就是通过字节码分析来提供元组参数信息的。这不容易做到,并且对内省工具来说是一个负担,因为它们必须知道 Python 字节码的工作原理(否则这是一个不必要的负担,因为所有其他类型的参数都不需要了解 Python 字节码)。
撇开分析字节码的困难不谈,依赖 Python 字节码还存在另一个问题。IronPython [3] 不使用 Python 的字节码。由于它基于 .NET 框架,它 instead 在函数的 func_code.co_code 属性中存储 MSIL [4]。这一事实使得 inspect.getargspec 函数在 IronPython 下运行时无法工作。目前尚不清楚其他 Python 实现是否受影响,但如果实现不是仅仅重新实现 Python 虚拟机,则合理地假设会受影响。
移除后不影响功能
如内省问题中所述,为了处理元组参数,函数的字节码以将参数解包为正确参数名称所需的字节码开头。这意味着实现元组参数不需要特殊支持,因此如果将其移除,功能不会有任何损失,只是可能失去了便利性(这在为何(据称)应保留中有所阐述)。
本 PEP 开头的示例函数可以很容易地重写为:
def fxn(a, b_c, d):
b, c = b_c
pass
并且功能丝毫不会受损。
规则的例外
在查看 Python 函数可以拥有的各种参数类型时,会注意到元组参数往往是例外而不是规则。
考虑 PEP 3102(仅限关键字参数)和 PEP 3107(函数注解)。这两个 PEP 都已被接受,并在函数签名中引入了新功能。然而,这两个 PEP 的新特性都不能应用于整个元组参数。PEP 3102 完全不支持元组参数(这很有道理,因为没有办法通过名称引用元组参数)。PEP 3107 允许对元组中的每个项目进行注解(例如,(x:int, y:int)),但不能对整个元组进行注解(例如,(x, y):int)。
元组参数的存在还将序列对象与映射对象在函数签名中分离。无法将映射对象(例如,字典)作为参数传入,并使其以与序列解包到元组参数相同的方式解包。
无信息量的错误消息
考虑以下函数:
def fxn((a, b), (c, d)):
pass
如果调用为 fxn(1, (2, 3)),则会收到错误消息 TypeError: unpack non-sequence。此错误消息完全没有告知哪个元组未正确解包。也没有任何迹象表明这是由于参数导致的结果。关于函数参数的其他错误消息明确说明了其与签名的关系:TypeError: fxn() takes exactly 2 arguments (0 given) 等。
使用率低
虽然我个人认识的少数 Python 程序员和 PyCon 2007 冲刺活动中的非正式调查表明,绝大多数人不知道这个特性,其余人只是不使用它,但需要一些硬数据来支持该特性使用不多的说法。
在 Python 代码仓库的 Lib/ 目录中遍历每一行,使用正则表达式 ^\s*def\s*\w+\s*\( 来检测函数和方法定义,在主干分支中发现了 22,252 个匹配项。
加上 .*,\s*\( 来查找包含元组参数的 def 语句,只发现了 41 个匹配项。这意味着在 def 语句中,只有 0.18% 的语句似乎使用了元组参数。
为何(据称)应保留
实际应用
在某些情况下,元组参数可能很有用。一个常见的例子是期望一个表示笛卡尔点的双项元组的代码。虽然能够为你解包 x 和 y 坐标确实很好,但论点是,这种微小的实际有用性被与元组参数相关的其他问题严重抵消了。正如移除后不影响功能中所示,它们的使用纯粹是实际的,并且无论如何都不能提供无法以其他方式轻松处理的独特功能。
参数的自文档化
有人认为,元组参数为预期为特定序列格式的参数提供了一种自文档化的方式。以我们从实际应用中引用的笛卡尔点示例为例,在函数中看到 (x, y) 作为参数,很明显该参数预期是一个长度为二的元组。
但是 Python 提供了其他几种方法来文档化参数的用途。文档字符串旨在提供足够的信息来解释预期的参数。元组参数可能会告诉你序列参数的预期长度,但它不会告诉你数据将用于什么。如果不是所有参数都是元组参数,还必须阅读文档字符串才能知道预期的其他参数。
函数注解(不适用于元组参数)也可以提供文档。因为注解可以是任何形式,所以以前的元组参数可以是一个带注解的单一参数,注解可以是 tuple、tuple(2)、Cartesian point、(x, y) 等。注解为文档化参数的预期用途提供了极大的灵活性,包括作为特定长度的序列。
过渡计划
为了将 Python 2.x 代码迁移到移除元组参数的 3.x,建议采取两个步骤。首先,当 Python 2.6 中的 Python 编译器遇到元组参数时,将发出适当的警告。这将像 Python 3.0 中相对于 Python 2.6 将发生的任何其他语法更改一样处理。
其次,2to3 重构工具 [1] 将获得一个 fixer [2],用于将元组参数转换为在函数中作为第一个语句解包的单个参数。新参数的名称将被更改。然后将新参数解包为最初在元组参数中使用的名称。这意味着以下函数:
def fxn((a, (b, c))):
pass
将被转换为:
def fxn(a_b_c):
(a, (b, c)) = a_b_c
pass
由于 lambda 表达式因其单表达式限制而使用元组参数,因此也必须支持它们。这通过将预期的序列参数绑定到单个参数,然后对该参数进行索引来实现:
lambda (x, y): x + y
将被转换为:
lambda x_y: x_y[0] + x_y[1]
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3113.rst
最后修改: 2025-02-01 08:59:27 GMT