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

Python 增强提案

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]

可以在 Python 代码中通过接受 (*args, **kwargs) 并手动解析参数来模拟这些语义。然而,这导致函数定义与函数合同接受的内容之间脱节。函数定义与参数处理的逻辑不匹配。

此外,/ 语法在 CPython 之外也被用于指定类似的语义(即,[1] [2]);因此,表明这些场景并非 CPython 和标准库所独有。

没有仅位置参数的问题

如果没有仅位置参数,库作者和 API 用户都会面临挑战。以下小节概述了每个实体遇到的问题。

库作者面临的挑战

对于位置或关键字参数,调用约定的混合并不总是可取的。作者可能希望通过不允许使用关键字参数调用 API 来限制 API 的使用,这会在作为公共 API 的一部分时暴露参数的名称。这种方法对于已经具有语义含义的必需函数参数(例如,namedtuple(typenames, field_names, …))或参数名称没有真正外部含义(例如,arg1arg2 等对于 min())时特别有用。如果 API 的调用者开始使用关键字参数,库作者就无法重命名参数,因为它将是一个破坏性更改。

仅位置参数可以通过逐个从 *args 中提取参数来模拟。然而,这种方法容易出错,并且如前所述,与函数定义不同步。函数的使用是模糊的,迫使用户查看 help()、相关的自动生成文档或源代码以了解函数合同接受哪些参数。

API 用户面临的挑战

用户在首次遇到仅位置表示法时可能会感到惊讶。鉴于它最近才被记录在案 [13],并且无法在 Python 代码中使用,这是预料之中的。由于这些原因,此表示法目前是一个异常,仅出现在用 C 开发的 CPython API 中。记录此表示法并使其能够在 Python 代码中使用将消除这种脱节。

此外,当前仅位置参数的文档不一致

  • 一些函数通过将其包含在嵌套方括号中来表示仅位置参数的可选组。[5]
  • 一些函数通过呈现具有不同参数数量的多个原型来表示仅位置参数的可选组。[6]
  • 一些函数使用上述两种方法。[4] [7]

当前文档没有区分的另一点是函数是否接受仅位置参数。open() 接受关键字参数;然而,ord() 不接受——仅通过阅读现有文档无法判断。

仅位置参数的优点

仅位置参数赋予库作者更多的控制权,以更好地表达 API 的预期用法,并允许 API 以安全、向后兼容的方式演进。此外,它使 Python 语言与现有文档以及各种“内置”和标准库函数的行为更加一致。

赋能库作者

库作者将可以灵活地更改仅位置参数的名称,而不会破坏调用者。这种灵活性降低了为必需参数或没有真正外部语义含义的参数选择合适的公共名称的认知负担。

仅位置参数在以下几种情况下很有用

  • 当函数接受任何关键字参数,但也可以接受位置参数时
  • 当参数没有外部语义含义时
  • 当 API 的参数是必需且明确时

一个关键场景是当函数接受任何关键字参数,但也可以接受位置参数时。突出的例子是 Formatter.formatdict.update。例如,dict.update 接受一个字典(位置),一个键/值对的可迭代对象(位置),或多个关键字参数。在这种情况下,如果字典参数不是仅位置参数,用户就不能使用函数定义中用于参数的名称,或者,反之,函数就无法轻易区分接收到的参数是字典/可迭代对象还是用于更新键/值对的关键字参数。

仅位置参数有用的另一个场景是当参数名称没有真正的外部语义含义时。例如,假设我们想创建一个函数,它将一种类型转换为另一种类型

def as_my_type(x):
    ...

参数的名称没有内在价值,并迫使 API 作者永远维护其名称,因为调用者可能会将 x 作为关键字参数传递。

此外,当 API 的参数是必需且在函数方面明确时,仅位置参数很有用。例如

def add_to_queue(item: QueueItem):
    ...

函数名称清楚地表明了预期的参数。关键字参数带来的好处微乎其微,并且也限制了 API 未来的演进。假设以后我们希望此函数能够接受多个项目,同时保持向后兼容性

def add_to_queue(items: Union[QueueItem, List[QueueItem]]):
    ...

或者通过使用参数列表来接受它们

def add_to_queue(*items: QueueItem):
    ...

作者将被迫始终保留原始参数名称,以避免可能破坏调用者。

通过能够指定仅位置参数,作者可以自由更改参数的名称,甚至将其更改为 *args,如前一个示例所示。标准库中有多个函数定义属于这一类别。例如,collections.defaultdict 的必需参数(在其文档中称为 default_factory)只能按位置传递。这种情况的一个特例是类方法的 self 参数:调用者在从类调用方法时,不希望通过关键字绑定到名称 self

io.FileIO.write(self=f, b=b"data")

事实上,用 C 实现的标准库中的函数定义通常将 self 作为仅位置参数

>>> help(io.FileIO.write)
Help on method_descriptor:

write(self, b, /)
    Write buffer b to file, return number of bytes written.

提高语言一致性

Python 语言将与仅位置参数更加一致。如果该概念是 Python 的一个正常特性,而不是扩展模块的独有特性,那么它将减少用户遇到仅位置参数函数时的困惑。一些主要的第三方包已经在其函数定义中使用了 / 符号 [1] [2]

弥合指定仅位置参数的“内置”函数与缺乏位置语法的纯 Python 实现之间的差距将提高一致性。/ 语法已经暴露在现有文档中,例如当内置函数和接口由参数诊所生成时。

另一个需要考虑的重要方面是 PEP 399,它强制标准库中模块的纯 Python 版本 必须 具有与 C 实现的加速器模块相同的接口和语义。例如,如果 collections.defaultdict 要有一个纯 Python 实现,它将需要利用仅位置参数来匹配其 C 对应项的接口。

基本原理

我们建议将仅位置参数作为一种新的语法引入 Python 语言。

新语法将使库作者能够进一步控制其 API 的调用方式。它将允许指定哪些参数必须作为仅位置参数调用,同时阻止它们作为关键字参数调用。

此前,(信息性)PEP 457 定义了语法,但范围更模糊。本 PEP 更进一步,通过论证语法并为函数定义中的 / 语法提供实现。

性能

除了上述优点之外,仅位置参数的解析和处理速度更快。这种性能优势可以在这个关于将关键字参数转换为位置参数的线程中得到证明:[11]。由于这种加速,最近出现了一种将内置函数从关键字参数中移除的趋势:最近,对 boolfloatlistinttuple 不兼容地进行了更改,以禁止关键字参数。

可维护性

提供一种在 Python 中指定仅位置参数的方法将使维护 C 模块的纯 Python 实现变得更容易。此外,定义函数的库作者将可以选择仅位置参数,如果他们认为传递关键字参数没有提供额外的清晰度。

这是一个在 Python 邮件列表中讨论充分、反复出现的话题

逻辑顺序

仅位置参数还有(次要的)好处,即在调用使用它们的接口时强制执行某些逻辑顺序。例如,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] 来约定性地指示参数为仅位置参数。

使用“__”作为逐参数标记

一些库和应用程序(如 mypyjinja)使用以双下划线(即 __)开头的名称作为约定来指示仅位置参数。我们拒绝了引入 __ 作为新语法的想法,因为

  • 这是一个向后不兼容的更改。
  • 它与当前声明仅关键字参数的方式不对称。
  • 查询 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 标记为仅位置参数,则无法将 xy 指定为关键字参数。鉴于目前关键字参数不能后跟位置参数,找到一种解决此限制的方法会增加混淆。因此,/ 会使前面的参数和后面的参数都成为仅位置参数。

致谢

此 PEP 的部分内容归功于 Larry Hastings 的 PEP 457

/ 用作仅位置参数和位置或关键字参数之间分隔符的功劳归于 Guido van Rossum,这是他于 2012 年提出的建议。[8]

简化语法讨论的功劳归于 Braulio Valdivieso。


来源: https://github.com/python/peps/blob/main/peps/pep-0570.rst

最后修改: 2025-02-01 08:59:27 GMT