Python 的默认参数为什么不能用 []?拆开 __defaults__ 里那个被所有人共享的列表
先看一段能让人怀疑人生的代码:
def add(x, lst=[]):
lst.append(x)
return lst
print(add(1)) # [1]
print(add(2)) # [1, 2] ← 为什么不是 [2]?
print(add(3)) # [1, 2, 3]
函数里一行"重置"的代码都没有,第二次调用却带着第一次的残留。这是 Python 最经典的坑之一,而且坑得很讲道理。
一、根因:默认参数在函数"定义时"求值一次
Python 里 def 是一条执行语句。解释器执行到 def add(...) 这一行时,就会把默认参数的值求值出来,然后挂到函数对象上,之后再也不重新算。
存哪儿?__defaults__:
>>> add.__defaults__
([1, 2, 3],)
>>> a = add(1); b = add(2)
>>> a is b
True
a is b 为 True,说明三次调用返回的是同一个列表对象。默认参数里放的不是"每次新建一个空列表"的指令,而是一个固定的列表引用——跟我们前几期讲的引用语义完全一回事:变量是标签,不是盒子。
所以 lst.append(x) 每次都在往同一个对象里塞东西。
二、这个坑不只坑列表
任何"定义时求值"的默认值都会被冻结:
import time
def log(msg, t=time.time()):
print(msg, t)
log('第一次')
time.sleep(2)
log('第二次') # 打印的时间戳和第一次一模一样
实测两次输出完全相同——time.time() 在 def 执行的那一刻就被算完了,后面再也不会更新。
三、正确写法:None 哨兵
def add(x, lst=None):
if lst is None:
lst = []
lst.append(x)
return lst
add(1) # [1]
add(2) # [2] ← 干净了
原理很简单:None 是不可变对象,冻不坏;每次调用时才在函数体里新建列表,于是每次都是新对象。
注意判断要写 if lst is None,而不是 if not lst——后者会把调用方传进来的合法空列表也给重置掉。
四、顺带一提:性能上它其实"很快"
有个反直觉的事实:那个有 bug 的版本比正确版本更快。实测调用 20 万次,复用同一列表只要 22ms,因为省掉了每次新建对象的开销。
但这是拿正确性换的速度,不能要。真要性能优化,应该用 collections.deque 或者预分配,而不是共享默认参数。
五、三条实用规则
- 默认参数只用不可变对象:
None、0、''、();想用 list/dict/set 就写None再在函数体里建。 - 判断用
is None,别用not。 - 默认值别放"会变的东西":
time.time()、random.random()、读取当前状态的函数调用,全都会被冻结在定义那一刻。
一句话总结:Python 的默认参数是"定义时求值的一次性快照",不是"调用时新建"的模板。 记住这条,这个坑就与你无关了。
(实测环境:CPython 3.13,Windows;代码可直接粘贴复现)

浙公网安备 33010602011771号