PEP 3113 – 元组参数解包的移除
- 作者:
- Brett Cannon <brett at python.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2007年3月2日
- Python 版本:
- 3.0
- 历史记录:
摘要
元组参数解包是指在函数签名中使用元组作为参数,以便自动解包序列参数。例如:
def fxn(a, (b, c), d):
pass
在签名中使用 (b, c)
要求函数的第二个参数为长度为2的序列(例如,[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 框架,所以它改为在函数的 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 程序员以及 2007 年 PyCon 冲刺赛中的程序员进行的非正式调查表明,绝大多数人不知道此功能,而其余的人只是不使用它,但需要一些确凿的数字来支持该功能使用率不高的说法。
使用正则表达式 ^\s*def\s*\w+\s*\(
迭代 Python 代码库中 Lib/
目录中的每一行以检测函数和方法定义,在主干中找到了 22,252 个匹配项。
添加 .*,\s*\(
以查找包含元组参数的 def
语句,只找到了 41 个匹配项。这意味着对于 def
语句,只有 0.18% 的语句似乎使用了元组参数。
为何应该(据称)保留
实际用途
在某些情况下,元组参数可能很有用。一个常见的例子是期望一个表示笛卡尔点的两个元素元组的代码。虽然能够为你解包 x 和 y 坐标确实很好,但论点是,这种少量实际用途远不及与元组参数相关的其他问题。如 移除后不会损失功能 中所示,它们的使用纯粹是实际用途,并且不会以任何方式提供其他方法无法轻松处理的独特功能。
参数的自文档化
有人认为,元组参数提供了一种为预期具有特定序列格式的参数进行自文档化的方法。使用我们来自 实际用途 的笛卡尔点示例,将 (x, y)
作为函数中的参数可以清楚地表明,期望一个长度为 2 的元组作为该参数的参数。
但是 Python 提供了几种其他方法来记录参数的用途。文档字符串旨在提供解释预期参数所需的所有信息。元组参数可能会告诉你序列参数的预期长度,但它不会告诉你这些数据将用于什么。如果并非所有参数都是元组参数,那么还必须阅读文档字符串才能知道期望的其他参数。
函数注解(不适用于元组参数)也可以提供文档。因为注解可以采用任何形式,所以曾经的元组参数可以是一个带有 tuple
、tuple(2)
、Cartesian point
、(x, y)
等注解的单个参数参数。注解为记录参数预期用途提供了极大的灵活性,包括为特定长度的序列提供注解。
迁移计划
为了将 Python 2.x 代码迁移到 3.x(其中元组参数已被移除),建议分两步进行。首先,当 Python 编译器在 Python 2.6 中遇到元组参数时,应发出正确的警告。这将被视为 Python 3.0 与 Python 2.6 相比发生的任何其他语法更改。
其次,2to3 重构工具 [1] 将获得一个修复程序 [2],用于将元组参数转换为单个参数,并在函数的第一个语句中进行解包。新参数的名称将被更改。然后,新参数将被解包到元组参数中最初使用的名称。这意味着以下函数
def fxn((a, (b, c))):
pass
将被翻译成
def fxn(a_b_c):
(a, (b, c)) = a_b_c
pass
由于 lambda 表达式受单一表达式限制,元组参数被 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