PEP 570:Positional-Only 参数

PEP 570:Positional-Only 参数

摘要

这个PEP建议引入一种新的语法,/,用于在Python函数定义中指定positional-only参数。

Positional-only参数没有外部可用的名称。当一个接受positional-only参数的函数被调用时,位置参数会根据它们自身的顺序映射到这些参数上。

(此处已添加圈子卡片,请到今日头条客户端查看)

在设计API(应用程序编程接口)时,库作者试图确保API的正确和预期用法。如果不能指定哪些参数只作为positional-only参数,那么库作者在选择合适的参数名称时就必须小心。即使对于必需的参数,或者当这些参数对于API的调用者没有外部语义含义时,也必须注意这一点。

  • 在本PEP中,我们讨论了:
  • Python中关于positional-only参数的历史和当前的语义
  • 没有positional-only参数时遇到的问题
  • 在没有语言本身对positional-only参数支持的情况下如何处理这些问题。
  • positional-only参数所带来的好处

在动机的范围内,我们接着讨论了:

  • 讨论为什么positional-only参数应该成为该语言的一个内在特性。
  • 提议标记positional-only参数的语法
  • 提出如何教授这个新特性
  • 指出拒绝的意见的详细细节

动机

Python中positional-only参数语义的历史

Python最初是支持positional-only参数的。语言的早期版本缺乏调用实参通过名称绑定到形参的函数的能力。在Python 1.0左右,参数语义变成了位置或关键字。从那时起,用户就可以按位置或通过函数定义中指定的关键字名称来为函数提供参数。

在Python的当前版本中,许多CPython的“内置函数”和标准库函数只接受positional-only参数。通过使用关键字参数调用其中一个函数,就可以很容易地观察到生成的语义:

PEP 570:Positional-Only 参数

pow()函数通过一个/标记来指明它的参数是positional-only参数。然而,这只是一个文档约定; Python开发人员并不能在代码中使用这种语法。

还有一些具有其他有趣语义的函数:

  • range(),一个重载函数, 它可以在它所需的参数的左边接受一个可选的参数。[4]
  • dict(),它的映射/迭代器参数是可选的,而语义必须是positional-only的。该参数的任何外部可见名称都将把该名称包含在**kwarg关键字可变参数字典中。[3]

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

此外,/语法在CPython之外还用于指定类似的语义(比如[1],[2]); 因此,这表明这些应用场景并不仅限于CPython和标准库。

没有positional-only参数时的问题

如果没有positional-only参数,那么一些API的库作者和用户就会面临一些挑战。下面的小节概述了每个实体会遇到的问题。

库作者面临的挑战

对于位置或关键字参数,混合调用约定并不总是可取的。库作者可能希望通过禁止使用关键字参数调用来限制API的使用,因为关键字参数在成为公共API的一部分时会公开参数名称。这种方法对于函数所需的具有语义意思的参数(例如, namedtuple(typenames, field_names, …))非常有用。或者当参数名没有真正的外部含义时(例如,对于min()函数中的arg1、arg2、…等参数)也非常有用。如果一个API的调用者开始使用关键字参数,那么库作者就不能重命名该参数,因为这将是一个破坏性的更改。

Positional-only参数可以通过从*args中逐个提取参数来进行模拟。但是,如前所述,这种方法很容易出错,并且与函数的定义不同步。函数的用法是不明确的,这就迫使用户要去查看help()、自动生成的相关文档或源代码,以了解函数实际上接受哪些参数。

使用API的用户面临的挑战

第一次遇到positional-only参数符号时,用户可能会感到惊讶。这是意料之中的,因为它是最近才被归档的[14],并且还不可能在Python代码中使用。由于这些原因,这种表示法目前只出现在用C开发的CPython 的API中。文档化这种表示法并使其能够在Python代码中使用将消除这种脱节。

此外,目前关于positional-only参数的文档并不一致:

  • 有些函数通过将位置参数放置在嵌套的方括号中来表示它是可选项。[5]
  • 有些函数通过使用不同数量的参数代表多个原型来表示可选的positional-only参数组。[6]
  • 有些函数同时使用上面两种方法。[4][7]

当前文档中没有区分的另一点是一个函数是否只接受positional-only参数。open()函数还接受关键字参数; 但是,ord()并不接受——目前,仅通过阅读现有文档还没法判断。

位置参数的好处

仅位置参数为库作者提供了更多的控制权,以便更好地表达一个API的预期用途,并允许该API以一种安全的、向后兼容的方式发展。此外,它使Python语言更符合现有文档以及各种“内置的”和标准库函数的行为。

赋予库作者更多的控制权

库作者可以在不中断调用程序的情况下灵活地更改positional-only参数的名称。这种灵活性减少了为所需参数或没有真正外部语义的参数选择合适的公开名称的认知负担。

Positional-only参数在以下几种情况下非常有用:

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

一个关键应用场景是函数接受任何关键字参数,但也可以接受位置参数。最突出的例子是Formatter.format和dict.update。例如,dict.update接受一个字典(位置上)、一个可迭代的键/值对(位置上)或多个关键字参数。在这个场景中,如果字典参数不是positional-only,用户就不能使用函数对参数定义的名称, 或者,相反,如果函数收到的用于更新该键/值对的参数是字典/可迭代对象或一个关键字参数,函数就不能很容易地区分它们。

Positional-only参数另一种有用的情况是,参数名称没有实际的外部语义。例如,假设我们想创建一个将一种类型转换为另一种类型的函数:

PEP 570:Positional-Only 参数

该参数名称不提供任何内在值,并强制API作者永久维护其名称,因为调用程序可能会将x作为一个关键字参数来传递。

此外,positional-only参数在一个API的参数是必需的且与函数无关时很有用。例如:

PEP 570:Positional-Only 参数

函数的名称清楚地表明了它所期望的参数。关键字参数提供的好处很少,而且还限制了API的未来发展。例如,在稍后的时间,我们希望这个函数能够接受多个项目,同时保持向后兼容性:

PEP 570:Positional-Only 参数

或者使用参数列表:

PEP 570:Positional-Only 参数

作者将被强制始终保留原始参数名,以避免潜在的突发调用程序。

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

PEP 570:Positional-Only 参数

实际上,C语言实现的标准库中的函数定义通常将self作为一个positional-only参数:

PEP 570:Positional-Only 参数

提高语言的一致性

Python语言将与positional-only参数更加一致。如果这个概念是Python的一个普通特性,而不是扩展模块独有的特性,那么这将减少用户在遇到带有positional-only参数的函数时的困惑。一些主要的第三方包已经在它们的函数定义中开始使用/符号[1][2]。

在指定positional-only参数的“内置”函数和缺乏位置性语法的纯Python实现之间建立桥梁将会提高Python语言的一致性。/语法已经在现有的文档中公开,比如当内置函数和接口由参数诊所(一个CPython 中C文件的预处理器)生成时。

另一个要考虑的重要方面是PEP 399,它要求标准库中模块的纯Python版本必须与以C实现的加速器模块具有相同的接口和语义。例如,似乎collections.defaultdict将需要一个它用来使用positional-only参数的纯Python实现来匹配其等价C实现的接口

根本原因

我们建议将positional-only参数作为一种新语法引入Python语言。

新的语法将使库作者能够进一步控制他们的API被调用的方式。它将允许你指定哪些参数必须作为positional-only来调用,同时防止它们被作为关键字参数调用。

以前,(信息性的)PEP 457定义了该语法,但是使用了一个更加模糊的范围。本PEP进一步证明了该语法的合理性,并为函数定义中的/ 语法提供了一个实现,从而将原始提案向前推进了一步。

性能

除了上述优点之外,positional-only参数的解析和处理速度更快。这种性能优势,可以通过这个将关键字参数转换为位置参数的线程来演示:[12]。由于这种加速,最近出现了一种将内置参数从关键字参数中移走的趋势:最近,向后不兼容的更改被用来禁止bool、float、list、int和tuple类型的关键字参数。

可维护性

在Python中提供一种指定positional-only参数的方法将使维护C模块的纯Python实现变得更容易。此外,如果定义函数的库作者决定传递一个关键字参数而提供额外的清晰度,那么他们可以选择positional-only参数。

在Python邮件列表中,这是一个讨论得很好且反复出现的主题:

  • September 2018: Anders Hovmöller: [Python想法] Positional-only 参数
  • February 2017: Victor Stinner: [Python想法] Positional-only 参数, 在3月份的继续讨论
  • February 2017: [10]
  • March 2012: [8]
  • May 2007: George Sakkis: [Python想法] Positional only 参数
  • May 2006: Benji York: [Python开发] Positional-only 参数

逻辑顺序

在调用使用positional-only参数的接口时,使用positional-only参数还有一个(较小的)好处,即强制执行某些逻辑顺序。例如,range函数按位置获取它的所有参数,并禁用如下形式:

PEP 570:Positional-Only 参数

以禁止在(唯一的)预定的顺序中使用关键字参数为代价:

PEP 570:Positional-Only 参数

兼容纯Python和C模块

Positional-only参数的另一个关键动机是PEP 399: 纯Python/C加速器模块兼容性要求。这个PEP声明道:

本PEP要求在这些实例中,C代码必须通过用于纯Python代码的测试套件,以便尽可能多地充当临时替换

如果C代码是使用现有的功能来实现的,即使用参数诊所和相关机制来实现只包含positional-only的参数,那么纯Python对等程序就不可能匹配其提供的接口和需求。这在CPython标准库和其他Python实现中的一些函数和类的接口之间造成了差异。例如:

PEP 570:Positional-Only 参数

其他Python实现可以手工复制CPython API,但这违背了PEP 399的精神,因为它要求添加到Python标准库中的所有模块必须具有具有相同接口和语义的纯Python实现,从而避免重复工作。

子类的一致性

另一种情况是,当子类覆盖了基类的方法并改变了参数的名称时,positional-only参数才能派上用场:

PEP 570:Positional-Only 参数

这种情况可以被认为是Liskov冲突——当需要的是基类的实例时,子类不能在上下文中使用。当子类有理由使用一个对其特定域名来说更合适的参数名称时,可以在重载方法中对参数进行重命名。(例如,当子类化映射来实现一个DNS查找缓存时,派生类可能不希望使用通用的参数名称“键”和“值”,而更愿意使用“主机”和“地址”)。使用具有positional-only参数的函数定义可以避免这个问题,因为用户将无法使用关键字参数来调用接口。一般来说,针对子类化的设计通常包括尚未编写的预测代码和作者无法控制的代码。对于库作者来说,拥有能够促使接口以向后兼容的方式进行发展的措施是非常有用的。

优化

支持positional-only参数的最后一个参数是那些允许一些新的优化的参数,比如在参数诊所中已经进行了优化的参数,因为参数需要按照严格的顺序进行传递。例如,CPython内部的METH_FASTCALL调用约定最近专门用于具有positional-only参数的函数,以消除处理空关键字的成本。由于positional-only参数的存在,在创建Python函数的评估框架时你也可以应用类似的性能改进。

规范

语法和语义

从一个很高的角度来看,出于示例的目的我们省略*args和**kwargs,函数定义的语法应该是这样的:

PEP 570:Positional-Only 参数

在上述例子的基础上,函数定义的新语法应该看起来像这样:

PEP 570:Positional-Only 参数

以下情况将适用:

  • /左边的所有参数都被视为 positional-only.
  • 如果函数定义中没有指定 /, 那么这个函数不接受任何 positional-only 参数.
  • 针对positional-only 参数的可选值的逻辑与针对 位置或关键字参数的逻辑相同的。
  • 一旦一个positional-only参数被定义为一个默认参数, 那么接下来的positional-only 参数和位置或关键字参数也需要作为默认设置。
  • 没有默认值的positional-only参数就是所需要的positional-only参数。

因此,以下是有效的函数定义:

PEP 570:Positional-Only 参数

就像今天一样,下面也是有效的函数定义:

PEP 570:Positional-Only 参数

但以下定义是无效的:

PEP 570:Positional-Only 参数

完整的语法规范

提议的语法规范的简化视图如下:

PEP 570:Positional-Only 参数

基于此PEP中的参考实现,typedarglist的新规则如下:

PEP 570:Positional-Only 参数

varargslist的新规则如下:

PEP 570:Positional-Only 参数

语义的其他情况

下面是该规范的一个有趣的推论。考虑一下这个函数定义:

PEP 570:Positional-Only 参数

没有可用的调用会使它返回True。例如:

PEP 570:Positional-Only 参数

但是使用/我们可以支持这一点:

PEP 570:Positional-Only 参数

现在上面的调用将返回True。

换句话说,positional-only参数的名称可以以**kwds的方式使用,而不会产生歧义。(另一个例子是,这对dict()和dict.update()的签名有好处。)

“/”作为分隔符的起源

使用/作为分隔符最初是由Guido van Rossum在2012年[8]提出的:

相关提议:使用'/'怎么样?它有点像‘*’的反义词,'*’的意思是‘关键字参数’,而'/'不是一个新字符。

如何教授这个语法

引入专用语法来标记positional-only参数与现有的强制关键字参数非常类似。同时教授这些概念可以简化教授用户可能遇到或设计的可能的函数定义的过程。

此PEP建议在Python文档中“更多关于定义函数的内容”[15]的章节中添加一个新的小节,在这一小节中讨论其余的参数类型。以下各段作为这些补充的一个草案。他们将介绍positional-only 参数和强制关键字参数的表示法。它不是很详细,也不应被看作是引入文档的最后版本。

默认情况下,参数可以通过位置或显式的关键字传递给Python函数。为了可读性和性能,限制传递参数的方式是有必要的,这样开发人员只需查看函数定义就可以确定参数是否按位置、按位置或关键字或者按关键字来传递。

函数定义可能如下所示:

PEP 570:Positional-Only 参数

其中/和*是可选的。如果使用了这些符号,则它们会通过参数传递给函数的方式来指明参数的种类: position -only、位置或关键字和强制关键字。关键字参数也称为命名参数。

位置或关键字参数

如果函数定义中不存在/和*,则参数可以通过位置或关键字来传递给函数。

Positional-Only 参数

我在再详细地看一下这个参数,我们是可以将某些参数标记为position –only的。如果参数是position –only的,则参数的顺序就很重要,并且参数不能通过关键字传递。位置参数将放在/(正斜杠)之前。这个/被用来在逻辑上将positional-only参数与其他参数分开。如果函数定义中没有/,则没有positional-only参数。

/后面的参数可以是位置或关键字( positional-or-keyword)参数或者关键字参数。

强制关键字参数

要将参数标记为强制关键字参数,指明参数必须通过关键字参数进行传递,请在参数列表中第一个强制关键字参数之前放置一个*。

函数示例

思考一下下面的示例函数定义,并密切关注/和*标记符:

PEP 570:Positional-Only 参数

第一个函数定义standard_arg是最常见的形式,它对调用约定没有任何限制,参数可以通过位置或关键字进行传递:

PEP 570:Positional-Only 参数

第二个函数pos_only_arg 被限制为只使用位置参数,因为函数定义中有一个 /:

PEP 570:Positional-Only 参数

第三个函数kwd_only_args只允许关键字参数,正如函数定义中的一个 *所指明的:

PEP 570:Positional-Only 参数

最后一个函数在同一个函数定义中使用了这三种调用约定:

PEP 570:Positional-Only 参数

概括

使用情况将决定在函数定义中使用哪些参数:

PEP 570:Positional-Only 参数

按照以下指导:

  • 如果名字无关紧要或没有实际意义, 以及当函数只有几个参数并且这些参数始终按照同样的顺序进行传递时,就使用positional-only参数 ,
  • 当参数名称有实际意义并且 通过这些显式的名称可以使函数定义更 容易 理解时,就使用强制关键字参数。

参考实现

可以使用一个通过CPython测试套件的初始实现来进行评估[11]。

这种实现的优点是处理positional-only参数的速度快,与强制关键字参数(PEP 3102)的实现保持一致,以及使受这种更改影响的所有工具和模块的实现变得更简单。

拒绝的想法

什么都不做

这一直都是一个选择——安于现状。虽然考虑到这一点,但上述优点还是值得添加到语言中。

装饰器

有人在Python想法[10]上建议为这个特性提供一个用Python编写的装饰器。

这种方法的优点是不会用额外的语法来污染函数定义。但是,我们决定拒绝这个想法,因为:

  • 它引入了一个与参数行为声明方式有关的不对称。
  • 它使得静态分析器和类型检查器很难安全地识别positional-only参数。它们需要查询装饰器列表的抽象语法树(AST)来按照名称或通过额外的启发式来识别正确的positional-only参数 ,而keyword-only参数是直接暴露在AST中的。为了使工具能正确识别positional-only参数,它们将需要执行模块去访问装饰器设置的任何元数据。
  • 声明过程的任何错误都将只在运行时报告。
  • 在很长的函数定义中识别positional-only 参数可能会更困难,因为这样一来用户就不得不去数这些参数并找出哪一个是受装饰器影响的最后一个参数。
  • / 语法已经被引入到了 C 函数中。这种不一致性将使实现任何处理这种语法的工具和模块变得更具挑战性——包括但不限于参数诊所、检查模块和ast模块。
  • 装饰器实现可能会附加一个运行时性能成本, 特别是在和 直接在解释器 中添加支持相比时。

Per-Argument 标记

per-argument标记是另一个语言固有的选项。该方法为每个参数添加了一个令牌,以表明它们是positional-only参数,并要求将这些参数放在一起。例子:

PEP 570:Positional-Only 参数

注意 .arg1和.arg2上的点(即 .)。虽然这种方法可能更容易阅读,但它被拒绝了,因为/作为一个显式标记与用于强制关键字参数的*更一致,而且也不容易出错。

应该注意的是,一些库已经使用了前导下划线[13]来按照惯例将参数指定为positional-only。

使用 "__" 作为一个 Per-Argument 标记

一些库和应用程序(如mypy或jinja)使用双下划线前缀的名称(即__)作为一个指明positional-only参数的约定。我们拒绝了将__作为一个新语法引入的想法,因为:

  • 它是一个向后不兼容的更改
  • 它与强制关键字参数目前的声明方式不对称。
  • 查询 positional-only 参数 的 AST 需要核实正常参数和检查它们 的名称, 而 keyword-only 参数有一个与它们(FunctionDef.args.kwonlyargs)有关联的属性。
  • 对每一个参数都要进行检查以知道positional-only 参数什么时候结束。
  • 这个标记更冗长, 会迫使你标记每个positional-only参数。
  • 它与其他双下划线前缀的用法相冲突,比如在类中调用名称粉碎规则。

用括号对Positional-Only参数进行分组

元组参数解包是Python2的一个特性,它允许在函数定义中使用一个元组作为参数。它支持对一个序列参数进行自动解包。一个例子是:

PEP 570:Positional-Only 参数

Python 3 (PEP 3113)中删除了元组参数解包。有人建议重用此语法来实现positional-only参数。我们拒绝了使用这种语法来指明positional-only参数的想法,原因如下:

  • 它的语法相对于强制关键字参数的声明是不对称 的。
  • Python 2 使用了这个语法,但考虑到这种语法的行为,它可能会带来混淆。对用户来说,将代码库移植到Python 2来使用这个特性是非常不可思议的。
  • 这个语法非常类似于元组文本(tuple literal)。这可能会引起额外的混淆,因为它可能会与元组声明混淆。

分隔符提案之后

在/之后标记位置参数是另一个要考虑的想法。但是,我们还没有找到一种方法来修改该标记之后的参数。不然,这个标记前的参数也将被标记为positional-only。例如:

PEP 570:Positional-Only 参数

如果我们定义了/来将z标记为positional-only,那么我们就不可能将x和y指定为关键字参数。由于此时关键字参数后面不能跟着位置参数,所以去寻找一种方法来解决这种限制将会增加混淆。

致谢

感谢Larry Hastings的PEP 457中包含了此PEP的一些内容。

感谢Guido van Rossum在2012年的一份提案中提出,使用/作为位置参数和位置或关键字参数之间的分隔符。[8]

感谢Braulio Valdivieso关于简化语法的讨论。

PEP 570:Positional-Only 参数

引用

[15]https://docs.python.org/3.7/tutorial/controlflow.html#more-on-defining-functions

版权

本文档已经被放置在公共域名上。

源码: https://github.com/python/peps/blob/master/pep-0570.rst

英文原文:https://www.python.org/dev/peps/pep-0570/

译者:一瞬

相关推荐