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

Python 增强提案

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 563PEP 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,应谨慎处理,因为它可能不代表自身,但除此之外,工具应看到现有代码未更改。

参考资料

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


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

最后修改: 2025-02-01 08:55:40 GMT