AIGC标识 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 或者预分配,而不是共享默认参数。

五、三条实用规则

  1. 默认参数只用不可变对象:None、0、''、();想用 list/dict/set 就写 None 再在函数体里建。
  2. 判断用 is None,别用 not。
  3. 默认值别放"会变的东西":time.time()、random.random()、读取当前状态的函数调用,全都会被冻结在定义那一刻。

一句话总结:Python 的默认参数是"定义时求值的一次性快照",不是"调用时新建"的模板。 记住这条,这个坑就与你无关了。

(实测环境:CPython 3.13,Windows;代码可直接粘贴复现)

posted @ 2026-11-03 09:05  小小张说故事  阅读(4)  评论(0)    收藏  举报