PEP 671 – 延迟绑定函数参数默认值的语法
- 作者:
- Chris Angelico <rosuav at gmail.com>
- 讨论至:
- Python-Ideas 讨论串
- 状态:
- 草案
- 类型:
- 标准跟踪
- 创建日期:
- 2021年10月24日
- Python 版本:
- 3.12
- 发布历史:
- 2021年10月24日, 2021年12月1日
摘要
函数参数可以有在函数定义期间计算并保存的默认值。本提案引入了一种新的参数默认形式,由在函数调用时评估的表达式定义。
动机
可选函数参数,如果省略,通常具有某种逻辑默认值。当此值依赖于其他参数,或需要在每次函数调用时重新评估时,目前没有一种简洁的方法可以在函数头中声明这一点。
目前合法的惯用法包括
# 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。
对于每个具有延迟绑定默认值的参数,特殊值 Ellipsis 被存储为值占位符,并且需要查询相应的额外信息。如果它是 None,则默认值确实是 Ellipsis;否则,它是一个描述性字符串,真实值在函数开始时计算。
当省略带有延迟绑定默认值的参数时,函数将以参数未绑定的状态开始。函数通过使用新的操作码 QUERY_FAST/QUERY_DEREF 测试每个带有延迟绑定默认值的参数来开始,如果未绑定,则评估原始表达式。此操作码(仅适用于快速局部变量和闭包变量)如果给定的局部变量有值,则将 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
最后修改: 2025-02-01 08:55:40 GMT