Python 默认参数只求值一次:可变默认值、None 与哨兵对象

举报
yd_254857454 发表于 2026/11/05 21:17:21 2026/11/05
【摘要】 为什么函数第二次调用会带上第一次的列表内容?本文用内存断言观察默认参数的求值时机,说明 None 方案如何避免意外共享,以及当 None 本身是合法输入时,如何用稳定的哨兵对象区分省略与显式传值。

一、只求值一次,是针对哪一次定义

函数签名里的 items=[] 很像“调用时如果没有传列表,就创建一个列表”。实际规则不同:默认表达式在执行函数定义时求值,同一个函数对象后续调用会复用已经计算好的默认值,不会因为又调用一次而重新执行这个表达式。

因此,“只求值一次”不是说整个进程里只执行一次所有同名默认表达式。如果一个外层函数每次运行都会执行内部的 def,内部默认表达式也会重新求值。至于结果是否为新对象,仍取决于表达式:[] 会建立列表,而一个指向已有列表的名称仍可能得到那个已有对象。

默认表达式里的函数调用也遵守这一规则。若将“取得当前值”的调用写在参数默认值中,它发生在定义执行时;需要每次缺省调用都重新取得值,就应把计算放到函数体相应分支中。本文用计数器验证时机,不依赖时钟或随机结果。

二、共享默认列表会累积修改

def collect_shared(item, items=[]):
    items.append(item)
    return items

first = collect_shared("A")
second = collect_shared("B")
assert first is second
assert first == ["A", "B"]

def collect_fresh(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

one = collect_fresh("one")
two = collect_fresh("two")
assert one is not two
assert one == ["one"]
assert two == ["two"]
supplied = []
assert collect_fresh("caller", supplied) is supplied
assert supplied == ["caller"]
print("默认列表共享与按调用新建验证通过")

第一段里,两次缺省调用拿到同一个列表。更容易忽略的是,first 并没有保存第一次返回值的独立快照;它一直指向这个列表,所以第二次追加之后,查看 first 也会看到两项。

第二段把默认值改为 None,在函数体内判断后建立新列表。每次进入这个分支,列表表达式才实际执行,因此两次缺省调用得到不同列表。若调用者显式提供列表,函数则直接修改并返回它;这是示例的接口约定,并不承诺保护调用方传入的数据。

三、None 方案需要明确接口含义

items=None 配合 if items is None: 会把“没有提供参数”和“明确传入 None”都解释为需要新建列表。只要这正是接口要表达的含义,它就是直接的处理方式。

不要为了简写而无条件改成 items = items or []。空列表同样是假值,这会把调用者明确传入的空列表替换成另一个列表,改变对象身份和后续修改落到哪里。上面的示例用 is None,所以传入空列表会保留它本身。

这也不意味着收集函数接受所有假值。它随后要调用 append(),输入仍需要符合预期容器契约;传入 0 或空字符串是否有效,是另一个类型和接口问题。缺省判断只负责识别需要建立默认对象的情况。

四、None 有业务含义时使用独立哨兵

有些接口需要区分三种情况:省略参数表示采用默认配置;显式传入 None 表示关闭该配置;传入其他值则保留调用方选择。这时再用 None 同时表示缺省,就丢失了区别。

_MISSING = object()

def resolve_option(value=_MISSING):
    if value is _MISSING:
        return {"mode": "default"}
    return value

assert resolve_option() == {"mode": "default"}
assert resolve_option(None) is None
assert resolve_option(0) == 0
assert resolve_option(False) is False
assert resolve_option("") == ""
supplied = []
assert resolve_option(supplied) is supplied
assert resolve_option() is not resolve_option()
print("省略参数与显式值区分验证通过")

object() 创建一个独立标记,默认参数保存这个对象。函数用 is 检查身份,而不是把用户值与某个特殊字符串或数值比较。这样既能保留显式 None,也不会因为值为假就走缺省分支;还避免了用户类型自定义相等比较对缺省识别的干扰。

这个标记需要保持稳定。不能在判断时重新调用 object(),也不应在函数定义后把 _MISSING 重新绑定为新对象:默认参数已经保存旧对象,比较目标却会改变。它只是接口标记;调用者若显式传入同一个哨兵对象,仍会被当作缺省,不能依靠它区分调用来源或建立权限边界。

五、名称重新绑定不等于修改默认对象

若定义时写的是 value=some_list,默认值保存的是当时求值所得对象。随后执行 some_list = other_list,只是让外部名称改指另一个对象,不会自动替换函数已经保存的默认值。相反,对原列表执行 append() 之类的原地修改,缺省调用仍可能看到变化。

所以需要检查的是默认值指向什么,以及哪些路径会修改它。风险不限于函数体直接调用 append():返回这个默认对象之后,调用方也可能修改它;把列表放进元组默认值里,也没有让内部列表变成不可变对象。

另一方面,函数体中把局部参数重新绑定到新对象,不等于原地修改默认对象。例如列表相加后赋回局部名称与在原列表上追加,效果不同。判断是否发生跨调用累积,应观察对象身份和实际修改方式,不能仅凭签名中出现容器字面量就推断结果。

如果共享状态本来就是设计目标,应明确其所有权、生命周期和使用约束。把状态交给清楚可见的对象或调用方管理,通常也更方便复位和验证;需要并发访问时,还要由相应设计处理同步。本例没有验证并发行为。

六、用调用顺序和身份检查验证约定

这类问题往往在单次调用中看不出来。验证时应在同一个函数对象上连续缺省调用,再检查返回对象是否相同;同时覆盖显式空列表、显式 None 和其他接口允许的值。需要每次建立新对象时,应同时检查内容与身份,而不是只比较两次初始内容是否相等。

本文两个代码块已在独立 CPython 3.9.6 进程运行,另有十二项标准库内存检查,覆盖默认列表复用、显式容器、按调用创建、None 含义、哨兵身份、名称绑定、默认表达式执行次数与顺序,以及重新绑定和嵌套可变对象。所有输入都为自制有限数据,没有读取私人项目、访问网络服务或使用实际时间作为断言依据。

参考:Python Tutorial 的 Default Argument Values;Python Language Reference 的 Function definitions;Built-in Constants 的 None;Built-in Functions 的 object;对象身份比较规则及 Programming FAQ 的默认值共享说明。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。