Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 671 – 延迟绑定函数参数默认值语法

作者:
Chris Angelico <rosuav at gmail.com>
讨论地址:
Python-Ideas 线程
状态:
草案
类型:
标准追踪
创建日期:
2021-10-24
Python 版本:
3.12
历史记录:
2021-10-24, 2021-12-01

目录

摘要

函数参数可以具有在函数定义期间计算并保存的默认值。此提案引入了一种新的参数默认值形式,它由在函数调用时评估的表达式定义。

动机

可选函数参数,如果省略,通常具有某种逻辑默认值。当此值取决于其他参数,或需要在每次函数调用时重新评估时,目前没有干净的方法在函数头部声明这一点。

目前合法的方式包括

# Very common: Use None and replace it in the function
def bisect_right(a, x, lo=0, hi=None, *, key=None):
    if hi is None:
        hi = len(a)

# Also well known: Use a unique custom sentinel object
_USE_GLOBAL_DEFAULT = object()
def connect(timeout=_USE_GLOBAL_DEFAULT):
    if timeout is _USE_GLOBAL_DEFAULT:
        timeout = default_timeout

# Unusual: Accept star-args and then validate
def add_item(item, *optional_target):
    if not optional_target:
        target = []
    else:
        target = optional_target[0]

在每种形式中,help(function) 无法显示真正的默认值。每个方式也都有额外的問題;使用 None 仅在 None 本身不是一个合理的函数参数时才有效,自定义哨兵需要一个全局常量;使用星号参数意味着可以给出多个参数。

规范

函数默认参数可以使用新的 => 符号定义

def bisect_right(a, x, lo=0, hi=>len(a), *, key=None):
def connect(timeout=>default_timeout):
def add_item(item, target=>[]):
def format_time(fmt, time_t=>time.time()):

表达式以源代码形式保存以供检查,用于评估表达式的字节码会被预置到函数体中。

值得注意的是,表达式是在函数的运行时范围内评估的,而不是在定义函数的范围内评估的(就像早绑定默认值一样)。这允许表达式引用其他参数。

多个延迟绑定参数从左到右评估,可以引用之前定义的值。顺序由函数定义,与关键字参数传递的顺序无关。

def prevref(word=”foo”, a=>len(word), b=>a//2): # 有效 def selfref(spam=>spam): # UnboundLocalError def spaminate(sausage=>eggs + 1, eggs=>sausage - 1): # 令人困惑,不要这样做 def frob(n=>len(items), items=[]): # 参见下方

评估顺序是从左到右;但是,实现可以选择在两个不同的阶段进行评估,首先是所有传递的参数和早绑定默认值,然后是延迟绑定默认值。否则,所有参数将严格按照从左到右的顺序分配。

被拒绝的拼写选择

虽然本文档指定了 name=>expression 这种语法,但其他拼写也同样合理。以下拼写也被考虑过

def bisect(a, hi=>len(a)):
def bisect(a, hi:=len(a)):
def bisect(a, hi?=len(a)):
def bisect(a, @hi=len(a)):

由于默认参数无论它们是早绑定还是延迟绑定,其行为都大体相同,因此所选语法 hi=>len(a) 故意与现有的早绑定语法相似。

拒绝 := 语法的其中一个原因是它与注解的交互。注解在默认值之前,因此在所有语法选项中,必须明确区分(对人和解析器)这是注解、默认值,还是两者兼有。备选语法 target:=expr 有被误解为 target:int=expr 并错误地省略注解的风险,因此可能会掩盖错误。所选语法 target=>expr 没有这个问题。

如何教授

应该始终首先教授早绑定默认参数,因为它们是评估参数的更简单、更有效的方式。在此基础上,延迟绑定参数与函数开头的代码大体等效

def add_item(item, target=>[]):

# Equivalent pseudocode:
def add_item(item, target=<OPTIONAL>):
    if target was omitted: target = []

一个简单的经验规则是:“target=expression” 在函数定义时评估,而 “target=>expression” 在函数调用时评估。无论哪种方式,如果在调用时提供了参数,则会忽略默认值。虽然这并不能完全解释所有细微差别,但它足以涵盖这里的重要区别(以及它们相似的事实)。

与其他提案的交互

PEP 661 试图解决与本提案相同的其中一个问题。它试图改进默认参数中哨兵值的文档记录,而本提案试图消除许多常见情况下对哨兵值的需要。 PEP 661 能够改进任意复杂函数的文档记录(它以 traceback.print_exception 作为其主要动机,该函数有两个参数,必须同时指定或都不指定);另一方面,如果可以由函数定义真正的默认值,那么许多常见情况将不再需要哨兵值。此外,专用哨兵对象可以作为字典查找键使用,而 PEP 671 不适用。

有时会提出通用的延迟评估系统(不要与 PEP 563PEP 649 混淆,这两个提案专门针对注解)。虽然表面上看起来延迟绑定参数默认值具有类似的性质,但它们实际上是无关且正交的思想,两者都可能对语言有价值。接受或拒绝本提案不会影响延迟评估提案的可行性,反之亦然。(泛化延迟评估和参数默认值之间的主要区别在于,参数默认值始终且仅在函数开始执行时评估,而延迟表达式仅在引用时实现。)

实现细节

以下内容与参考实现有关,不一定属于规范的一部分。

参数默认值(位置或关键字)既有其值(如已保留),又有一段额外的信息。对于位置参数,额外信息存储在 __defaults_extra__ 中的元组中,对于仅关键字参数,存储在 __kwdefaults_extra__ 中的字典中。如果此属性为 None,则等效于所有参数默认值为 None

对于每个具有延迟绑定默认值的 parameter,特殊值 Ellipsis 被存储为值占位符,并且需要查询相应的额外信息。如果它为 None,则默认值实际上为值 Ellipsis;否则,它是一个描述性字符串,并且真值在函数开始时计算。

当省略具有延迟绑定默认值的 parameter 时,函数将以 parameter 未绑定状态开始。函数从使用新的操作码 QUERY_FAST/QUERY_DEREF 测试每个具有延迟绑定默认值的 parameter 开始,如果未绑定,则评估原始表达式。此操作码(仅适用于快速局部变量和闭包变量)如果给定的局部变量有值,则将 True 推送到堆栈中,否则将 False 推送到堆栈中 - 这意味着如果 LOAD_FAST 或 LOAD_DEREF 会引发 UnboundLocalError,则会推送 False,如果会成功,则会推送 True。

只要被引用者具有来自参数或早绑定默认值的值,就可以允许无序变量引用。

成本

当不使用延迟绑定参数默认值时,以下成本应该是唯一发生的成本

  • 函数对象需要两个额外的指针,它们将为 NULL
  • 编译代码和构建函数会进行额外的标志检查
  • 使用 Ellipsis 作为默认值将需要运行时验证以查看是否存在延迟绑定默认值。

这些成本预计会很小(在 64 位 Linux 上,这会将所有函数对象从 152 字节增加到 168 字节),当不使用延迟绑定默认值时,实际上没有运行时成本。

向后不兼容

在不使用延迟绑定默认值的情况下,行为应该保持一致。如果找到 Ellipsis,应格外小心,因为它可能不代表自身,但除此之外,工具应该看到现有的代码没有改变。

参考文献

https://github.com/rosuav/cpython/tree/pep-671


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

最后修改日期:2023-09-09 17:39:29 GMT