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 563 和 PEP 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,应格外小心,因为它可能不代表自身,但除此之外,工具应该看到现有的代码没有改变。
参考文献
版权
本文档属于公有领域或根据 CC0-1.0-Universal 许可证发布,以较宽松的许可为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0671.rst
最后修改日期:2023-09-09 17:39:29 GMT