bigywater.tech
Python · 16

可变默认参数:为什么 def f(x=[]) 会留下历史数据

连续三次调用append_to(item)不传target,验证默认列表在调用之间是否被共享,用__defaults__确认背后的对象关系。

同一对象
三次调用返回的是同一个列表
只创建1次
默认值只在def执行时求值一次
None哨兵
修复的标准写法

做的事:一个"看起来很正常"的函数,连续调用三次

默认参数是个列表,直觉上以为每次调用不传target就该是个新的空列表。

代码def append_to(item, target=[]): target.append(item) return target r1 = append_to(1) r2 = append_to(2) r3 = append_to(3)

验证:第二次调用带着第一次的数据

三次调用,一次比一次"多"。

结果第1次调用append_to(1): [1] 第2次调用append_to(2)(没传target): [1, 2] ← 包含了上次的1! 第3次调用append_to(3): [1, 2, 3] r1 is r2: True r2 is r3: True

每一次返回的都是同一个列表对象——不是"看起来一样",是的同一个对象,is 判断全部为True。

验证:默认参数对象只在函数定义时被创建一次

用__defaults__直接看函数对象自己保存的默认值。

结果默认参数对象的id全程没变: 4342877056 append_to.__defaults__: ([1, 2, 3],)

原因是:def 语句在被执行(也就是函数被定义)的那一刻,[] 这个默认值表达式只被求值了一次,结果被存进函数对象自己的 __defaults__ 属性里,之后每次调用只要没传 target,用的都是存在 __defaults__ 里的那同一个列表——它不会随着每次调用重新创建,函数对象本身在整个程序运行期间只有一份 __defaults__。

这也是为什么三次调用后 __defaults__ 里变成了 ([1, 2, 3],)——每次 target.append(item) 修改的都是这同一个列表,痕迹会一直累积下去,只要这个函数对象还活着。

验证:正确的修复方式

用None做哨兵,把"创建新列表"这件事挪到函数体内部。

代码与结果def append_to_fixed(item, target=None): if target is None: target = [] target.append(item) return target f1 = append_to_fixed(1) f2 = append_to_fixed(2) 结果: f1=[1], f2=[2], f1 is f2 为False

修复的关键:默认值改成不可变的 None(True单例,不存在"被共享后累积历史数据"的问题),真正的空列表 [] 挪到函数体内部,每次调用函数体都会重新执行一遍 target = [],创建一个全新的对象——这才是符合直觉的"每次调用都是独立的一份"。

分步演示:可变默认参数是怎么累积历史数据的

每一步都是本文上面用Python 3.14跑出来的原始结果。

默认参数对象只在def执行时创建一次,之后被反复复用
这一步新验证的调用

    本文用到的工具

    • 本机 Python 3.14.7
    • __defaults__ / id():验证默认参数对象只创建一次
    京ICP备2026067502号-1京公网安备11010502063487号