PEP 570 – Python 位置参数
- 作者:
- Larry Hastings <larry at hastings.org>,Pablo Galindo <pablogsal at python.org>,Mario Corchero <mariocj89 at gmail.com>,Eric N. Vander Weele <ericvw at gmail.com>
- BDFL 代表:
- Guido van Rossum <guido at python.org>
- 讨论邮件列表:
- Discourse 线程
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2018 年 1 月 20 日
- Python 版本:
- 3.8
摘要
本 PEP 提案引入一种新的语法 /
,用于在 Python 函数定义中指定位置参数。
位置参数没有外部可用的名称。当调用接受位置参数的函数时,位置参数仅根据其顺序映射到这些参数。
在设计 API(应用程序编程接口)时,库作者会尝试确保 API 的正确和预期使用。如果没有指定哪些参数是位置参数的能力,库作者在选择合适的参数名称时必须小心。即使对于必需参数或参数对 API 调用者没有外部语义含义的情况,也必须小心谨慎。
在本 PEP 中,我们讨论
- Python 的历史以及当前位置参数的语义
- 没有位置参数遇到的问题
- 如何在没有语言固有位置参数支持的情况下处理这些问题
- 拥有位置参数的好处
在动机的背景下,我们然后
- 讨论为什么位置参数应该成为语言固有的特性
- 提出标记位置参数的语法
- 介绍如何教授此新功能
- 更详细地说明被拒绝的想法
动机
Python 中位置参数语义的历史
Python 最初支持位置参数。早期版本的语言缺乏通过名称将参数绑定到参数来调用函数的能力。在 Python 1.0 左右,参数语义更改为位置或关键字。从那时起,用户就可以通过函数定义中指定的位置或关键字名称来向函数提供参数。
在当前版本的 Python 中,许多 CPython “内置”和标准库函数仅接受位置参数。通过使用关键字参数调用其中一个函数,可以轻松观察到结果语义
>>> help(pow)
...
pow(x, y, z=None, /)
...
>>> pow(x=5, y=3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pow() takes no keyword arguments
pow()
通过 /
标记表达其参数是位置参数。但是,这只是一个文档约定;Python 开发人员无法在代码中使用此语法。
有一些函数具有其他有趣的语义
range()
是一个重载函数,它接受其必需参数左侧的一个可选参数。 [4]dict()
,其映射/迭代器参数是可选的,并且在语义上必须是位置参数。此参数的任何外部可见名称都会隐藏该名称进入**kwarg
关键字可变参数字典。 [3]
可以通过接受 (*args, **kwargs)
并手动解析参数来在 Python 代码中模拟这些语义。但是,这会导致函数定义与其函数合同接受的内容之间脱节。函数定义与参数处理的逻辑不匹配。
此外,/
语法在 CPython 之外用于指定类似的语义(即 [1] [2]);因此,表明这些场景并非 CPython 和标准库独有。
没有位置参数的问题
如果没有位置参数,库作者和 API 用户都会面临挑战。以下小节概述了每个实体遇到的问题。
API 用户面临的挑战
用户在第一次遇到位置参数表示法时可能会感到惊讶。鉴于它最近才被记录在案 [13] 并且无法在 Python 代码中使用,这是可以预期的。由于这些原因,此表示法目前是一个异常值,仅出现在用 C 开发的 CPython API 中。记录此表示法并使其可以在 Python 代码中使用将消除这种脱节。
此外,当前位置参数的文档不一致
当前文档没有区分的另一点是函数是否采用位置参数。open()
接受关键字参数;但是,ord()
则不接受——仅通过阅读现有文档无法分辨。
位置参数的优势
位置参数赋予库作者更多控制权,以更好地表达 API 的预期用法,并允许 API 以安全、向后兼容的方式发展。此外,它使 Python 语言与现有文档以及各种“内置”和标准库函数的行为更加一致。
改进语言一致性
Python 语言在使用位置限定参数方面可以更加一致。如果这个概念成为 Python 的一项普通特性,而不是扩展模块独有的特性,那么它将减少用户在遇到使用位置限定参数的函数时产生的困惑。一些主要的第三方包已经在它们的函数定义中使用了 /
符号 [1] [2]。
弥合“内置”函数(指定位置限定参数)和缺乏位置语法的纯 Python 实现之间的差距将提高一致性。例如,当内置函数和接口由参数诊所生成时,/
语法已在现有文档中公开。
另一个需要考虑的重要方面是 PEP 399,它规定标准库中模块的纯 Python 版本*必须*与用 C 实现的加速器模块具有相同的接口和语义。例如,如果 collections.defaultdict
需要一个纯 Python 实现,则需要使用位置限定参数来匹配其 C 对应物的接口。
基本原理
我们建议将位置限定参数作为一种新的语法引入 Python 语言。
新的语法将使库作者能够进一步控制其 API 的调用方式。它允许指定哪些参数必须作为位置限定参数调用,同时阻止它们作为关键字参数调用。
之前,(信息性的)PEP 457 定义了语法,但范围模糊得多。本 PEP 将原始提案更进一步,通过论证语法并在函数定义中为 /
语法提供实现。
性能
除了上述好处之外,位置限定参数的解析和处理速度更快。这个性能优势可以在关于将关键字参数转换为位置参数的这个线程中得到证明:[11]。由于这种加速,最近出现了将内置函数从关键字参数转向位置参数的趋势:最近,做了一些向后不兼容的更改,禁止对 bool
、float
、list
、int
、tuple
使用关键字参数。
可维护性
提供一种在 Python 中指定位置限定参数的方法将使维护 C 模块的纯 Python 实现变得更容易。此外,定义函数的库作者可以选择位置限定参数,如果他们确定传递关键字参数不会提供额外的清晰度。
这是一个在 Python 邮件列表中经常讨论的反复出现的话题。
- 2018 年 9 月:Anders Hovmöller:[Python-ideas] 位置限定参数
- 2017 年 2 月:Victor Stinner:[Python-ideas] 位置限定参数,讨论持续到 3 月
- 2017 年 2 月:[9]
- 2012 年 3 月:[8]
- 2007 年 5 月:George Sakkis:[Python-ideas] 仅位置参数
- 2006 年 5 月:Benji York:[Python-Dev] 仅位置参数
逻辑排序
位置限定参数还具有(次要的)好处,即在调用使用它们的接口时强制执行某些逻辑顺序。例如,range
函数以位置方式获取所有参数,并且不允许像
range(stop=5, start=0, step=2)
range(stop=5, step=2, start=0)
range(step=2, start=0, stop=5)
range(step=2, stop=5, start=0)
这样的形式,但代价是不允许对(唯一)预期顺序使用关键字参数。
range(start=0, stop=5, step=2)
纯 Python 和 C 模块的兼容性
位置限定参数的另一个关键动机是 PEP 399:纯 Python/C 加速器模块兼容性要求。本 PEP 指出
本 PEP 要求在这些情况下,C 代码必须通过用于纯 Python 代码的测试套件,以尽可能地充当直接替换。
如果 C 代码使用现有的功能通过参数诊所和相关机制来实现位置限定参数,那么纯 Python 对应方将无法匹配提供的接口和要求。这在 CPython 标准库中某些函数和类的接口与其他 Python 实现之间造成了差异。例如
$ python3 # CPython 3.7.2
>>> import binascii; binascii.crc32(data=b'data')
TypeError: crc32() takes no keyword arguments
$ pypy3 # PyPy 6.0.0
>>>> import binascii; binascii.crc32(data=b'data')
2918445923
其他 Python 实现可以手动复制 CPython API,但这违背了 PEP 399 的精神,即避免重复工作,强制要求添加到 Python 标准库中的所有模块**必须**具有具有相同接口和语义的纯 Python 实现。
子类中的一致性
位置限定参数提供好处的另一个场景是,当子类重写基类的某个方法并更改旨在作为位置参数的参数名称时。
class Base:
def meth(self, arg: int) -> str:
...
class Sub(Base):
def meth(self, other_arg: int) -> str:
...
def func(x: Base):
x.meth(arg=12)
func(Sub()) # Runtime error
这种情况可以被认为是 Liskov 违规——当期望使用基类实例时,不能在上下文中使用子类。当子类有理由使用更适合子类特定域的参数名称时,重载方法时重命名参数可能会发生(例如,当子类化 Mapping
以实现 DNS 查找缓存时,派生类可能不希望使用通用参数名称“key”和“value”,而是使用“host”和“address”)。使用位置限定参数定义此函数可以避免此问题,因为用户将无法使用关键字参数调用接口。一般来说,为子类化设计通常需要预测尚未编写的代码以及作者无法控制的代码。拥有可以促进接口向后兼容演化的措施对于库作者来说将非常有用。
优化
支持位置限定参数的最后一个论点是,它们允许一些新的优化,例如参数诊所中已经存在的一些优化,因为参数预计将按严格的顺序传递。例如,CPython 的内部 METH_FASTCALL
调用约定最近已专门用于具有位置限定参数的函数,以消除处理空关键字的成本。由于位置限定参数,在创建 Python 函数的评估框架时,可以应用类似的性能改进。
规范
语法和语义
从“一万英尺的高度”来看,省略 *args
和 **kwargs
以进行说明,函数定义的语法将如下所示
def name(positional_or_keyword_parameters, *, keyword_only_parameters):
在此示例的基础上,函数定义的新语法将如下所示
def name(positional_only_parameters, /, positional_or_keyword_parameters,
*, keyword_only_parameters):
以下将适用
/
左侧的所有参数都被视为位置限定参数。- 如果在函数定义中未指定
/
,则该函数不接受任何位置限定参数。 - 位置限定参数的可选值周围的逻辑与位置或关键字参数相同。
- 一旦使用默认值指定了位置限定参数,则以下位置限定参数和位置或关键字参数也需要具有默认值。
- 没有默认值的位置限定参数是*必需的*位置限定参数。
因此,以下将是有效的函数定义
def name(p1, p2, /, p_or_kw, *, kw):
def name(p1, p2=None, /, p_or_kw=None, *, kw):
def name(p1, p2=None, /, *, kw):
def name(p1, p2=None, /):
def name(p1, p2, /, p_or_kw):
def name(p1, p2, /):
就像今天一样,以下将是有效的函数定义
def name(p_or_kw, *, kw):
def name(*, kw):
而以下将是无效的
def name(p1, p2=None, /, p_or_kw, *, kw):
def name(p1=None, p2, /, p_or_kw=None, *, kw):
def name(p1=None, p2, /):
完整语法规范
提议的语法规范的简化视图为
typedargslist:
tfpdef ['=' test] (',' tfpdef ['=' test])* ',' '/' [',' # and so on
varargslist:
vfpdef ['=' test] (',' vfpdef ['=' test])* ',' '/' [',' # and so on
根据本 PEP 中的参考实现,typedarglist
的新规则将是
typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* ',' '/' [',' [tfpdef ['=' test] (',' tfpdef ['=' test])* [',' [
'*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
| '**' tfpdef [',']]]
| '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
| '**' tfpdef [',']] ] )| (
tfpdef ['=' test] (',' tfpdef ['=' test])* [',' [
'*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
| '**' tfpdef [',']]]
| '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
| '**' tfpdef [','])
而 varargslist
的新规则将是
varargslist: vfpdef ['=' test ](',' vfpdef ['=' test])* ',' '/' [',' [ (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' [
'*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
| '**' vfpdef [',']]]
| '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
| '**' vfpdef [',']) ]] | (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' [
'*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
| '**' vfpdef [',']]]
| '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
| '**' vfpdef [',']
)
语义特殊情况
以下是规范的一个有趣的推论。考虑此函数定义
def foo(name, **kwds):
return 'name' in kwds
没有可能的调用会使其返回 True
。例如
>>> foo(1, **{'name': 2})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>
但是使用 /
我们可以支持这一点
def foo(name, /, **kwds):
return 'name' in kwds
现在上述调用将返回 True
。
换句话说,位置限定参数的名称可以在 **kwds
中使用,而不会产生歧义。(作为另一个示例,这有利于 dict()
和 dict.update()
的签名。)
“/”作为分隔符的由来
使用 /
作为分隔符最初是由 Guido van Rossum 在 2012 年提出的 [8]
备选方案:如何使用“/”?它有点像“*”的反义词,后者表示“关键字参数”,而“/”不是新字符。
如何教授此功能
引入专用语法来标记位置限定参数与现有的仅关键字参数非常类似。将这些概念一起教授可能会*简化*如何教授用户可能遇到或设计的可能的函数定义。
本 PEP 建议在 Python 文档中添加一个新的小节,位于 “关于函数定义的更多内容” 部分,在该部分讨论了其余的参数类型。以下段落作为这些新增内容的草稿。它们将介绍位置限定参数和仅关键字参数的表示法。它不打算详尽无遗,也不应被视为要纳入文档的最终版本。
默认情况下,可以通过位置或显式地通过关键字将参数传递给 Python 函数。为了提高可读性和性能,限制参数的传递方式是有意义的,这样开发人员只需要查看函数定义即可确定项目是按位置传递、按位置或关键字传递还是按关键字传递。
函数定义可能如下所示
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
其中 /
和 *
是可选的。如果使用,这些符号通过参数如何传递给函数来指示参数的类型:位置限定、位置或关键字以及仅关键字。关键字参数也称为命名参数。
位置或关键字参数
如果函数定义中不存在 /
和 *
,则可以通过位置或关键字将参数传递给函数。
位置参数
更详细地来看,可以将某些参数标记为仅限位置。如果为仅限位置,则参数的顺序很重要,并且不能通过关键字传递参数。仅限位置的参数将放置在 /
(正斜杠)之前。 /
用于在仅限位置的参数与其余参数之间进行逻辑上的分隔。如果函数定义中没有 /
,则没有仅限位置的参数。
/
之后的参数可以是位置或关键字或仅限关键字。
仅限关键字参数
要将参数标记为仅限关键字,表示必须通过关键字参数传递参数,请在参数列表中第一个仅限关键字参数之前放置一个 *
。
函数示例
考虑以下示例函数定义,密切注意标记 /
和 *
>>> def standard_arg(arg):
... print(arg)
...
>>> def pos_only_arg(arg, /):
... print(arg)
...
>>> def kwd_only_arg(*, arg):
... print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
... print(pos_only, standard, kwd_only)
第一个函数定义 standard_arg
,最常见的形式,对调用约定没有限制,并且可以通过位置或关键字传递参数。
>>> standard_arg(2)
2
>>> standard_arg(arg=2)
2
第二个函数 pos_only_arg
仅限于使用位置参数,因为函数定义中有一个 /
。
>>> pos_only_arg(1)
1
>>> pos_only_arg(arg=1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got an unexpected keyword argument 'arg'
第三个函数 kwd_only_args
仅允许关键字参数,如函数定义中的 *
所示。
>>> kwd_only_arg(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given
>>> kwd_only_arg(arg=3)
3
最后一个函数在同一个函数定义中使用了所有三种调用约定。
>>> combined_example(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given
>>> combined_example(1, 2, kwd_only=3)
1 2 3
>>> combined_example(1, standard=2, kwd_only=3)
1 2 3
>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: combined_example() got an unexpected keyword argument 'pos_only'
回顾
用例将决定在函数定义中使用哪些参数。
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
作为指导
- 如果名称无关紧要或没有意义,并且只有几个参数始终以相同的顺序传递,则使用仅限位置的参数。
- 如果名称有意义并且通过显式使用名称可以使函数定义更易于理解,则使用仅限关键字的参数。
参考实现
已提供了一个通过 CPython 测试套件的初始实现以供评估 [10]。
此实现的好处包括处理仅限位置的参数的速度、与仅限关键字参数的实现一致性(PEP 3102)以及所有受此更改影响的工具和模块的更简单的实现。
被拒绝的想法
不做任何事
始终可以选择——现状。虽然我们考虑过这一点,但上述好处值得添加到语言中。
装饰器
有人在 python-ideas 上建议 [9] 为此功能提供一个用 Python 编写的装饰器。
这种方法的好处是不需要用额外的语法污染函数定义。但是,我们决定拒绝这个想法,因为
- 它引入了参数行为声明方式的不对称性。
- 它使静态分析器和类型检查器难以安全地识别仅限位置的参数。它们需要查询 AST 以获取装饰器列表并按名称或使用额外的启发式方法识别正确的装饰器,而仅限关键字的参数直接在 AST 中公开。为了使工具能够正确识别仅限位置的参数,它们需要执行模块以访问装饰器设置的任何元数据。
- 声明中的任何错误都只会在运行时报告。
- 在较长的函数定义中,识别仅限位置的参数可能更加困难,因为它迫使用户对它们进行计数以知道受装饰器影响的最后一个参数是哪个。
/
语法已经引入 C 函数。这种不一致性将使实现任何处理此语法的工具和模块(包括但不限于参数诊所、inspect 模块和ast
模块)更具挑战性。- 装饰器实现可能会带来运行时性能成本,特别是与直接向解释器添加支持相比。
每个参数的标记
每个参数的标记是另一个语言内在选项。此方法为每个参数添加一个标记以指示它们是仅限位置的,并要求这些参数放在一起。示例
def (.arg1, .arg2, arg3):
请注意 .arg1
和 .arg2
上的点(即 .
)。虽然这种方法可能更容易阅读,但它已被拒绝,因为 /
作为显式标记与 *
(用于仅限关键字的参数)一致,并且错误更少。
需要注意的是,一些库已经使用前导下划线 [12] 来约定俗成地将参数指示为仅限位置的。
使用“__”作为每个参数的标记
一些库和应用程序(如 mypy
或 jinja
)使用以双下划线(即 __
)为前缀的名称作为约定来指示仅限位置的参数。我们已经拒绝了将 __
作为新语法引入的想法,因为
- 它是不兼容的更改。
- 它与当前声明仅限关键字参数的方式不对称。
- 查询 AST 以获取仅限位置的参数需要检查普通参数并检查其名称,而仅限关键字的参数具有与其关联的属性(
FunctionDef.args.kwonlyargs
)。 - 需要检查每个参数才能知道仅限位置的参数在哪里结束。
- 标记更冗长,需要标记每个仅限位置的参数。
- 它与双下划线前缀的其他用途(如在类中调用名称混淆)发生冲突。
使用括号对位置参数进行分组
元组参数解包是 Python 2 中的一个特性,它允许在函数定义中使用元组作为参数。它允许自动解包序列参数。例如
def fxn(a, (b, c), d):
pass
元组参数解包在 Python 3 中被移除(PEP 3113)。有人提议重用此语法来实现仅限位置的参数。由于以下几个原因,我们拒绝了此语法来指示仅限位置的参数
- 此语法在声明仅限关键字参数的方式方面是不对称的。
- Python 2 使用此语法,这可能会导致对该语法的行为产生混淆。对于移植使用此功能的 Python 2 代码库的用户来说,这将令人惊讶。
- 此语法与元组字面量非常相似。这可能会导致额外的混淆,因为它可能会与元组声明混淆。
分隔符后提案
在 /
之后标记位置参数是另一个考虑过的想法。但是,我们无法找到修改标记后参数的方法。否则,将迫使标记前的参数也成为仅限位置的参数。例如
def (x, y, /, z):
如果我们定义 /
将 z
标记为仅限位置,则无法将 x
和 y
指定为关键字参数。找到解决此限制的方法会增加混淆,因为目前关键字参数后面不能跟位置参数。因此, /
将使前面和后面的参数都成为仅限位置的参数。
致谢
此 PEP 中部分内容的版权归 Larry Hastings 所有,他在 PEP 457 中提出了此内容。
将 /
用作仅限位置参数和位置或关键字参数之间的分隔符的版权归 Guido van Rossum 所有,他在 2012 年的一个提案中提出了此内容。 [8]
关于简化语法的讨论的版权归 Braulio Valdivieso 所有。
版权
本文档已进入公有领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0570.rst
上次修改时间: 2023-09-09 17:39:29 GMT