Python 里 def f(x=[]) 为什么每次调用都共用同一个列表?拆开 __defaults__ 和函数对象创建时那一次求值
几乎每个 Python 教程都会警告「别用可变对象当默认参数」,但多数只给结论不给机制:为什么是共享的?那个列表到底存在哪儿?什么时候被求值一次?
一句话结论:默认参数不是每次调用时创建的,而是在 def 语句执行、函数对象被创建那一刻求值一次,之后作为 __defaults__ 元组挂在函数对象上,被所有调用共享。[] 从来不在字节码常量池里,它活在函数对象里——所以 f() 调一万次,改的都是同一个列表。
下面数据来自本机 CPython 3.13.14 实测(d54-probe.py)。
一、先复现:三次调用,同一个列表
def f(x=[]):
x.append(len(x))
return x
r1, r2, r3 = f(), f(), f()
print(r1, r2, r3, r1 is r2 is r3)
print(f.__defaults__, id(f.__defaults__[0]))
实测输出:
[0, 1, 2] [0, 1, 2] [0, 1, 2] True
([0, 1, 2],) 2821577192256
注意 r1、r2、r3 打印出来都是 [0, 1, 2]——不是「三次分别得到 [0]、[0,1]、[0,1,2]」,而是三个变量指向同一个对象,最后状态就是 [0, 1, 2]。r1 is r2 is r3 为 True。
再验证一次:
print(id(f()) == id(f.__defaults__[0]))
True
每调用一次 f(),返回的正是 __defaults__[0] 那一个列表。所谓「默认参数被污染」,本质是「函数对象的一个属性被修改了」。
二、那个列表到底存在哪儿
很多人以为默认值会像字符串、数字一样进 co_consts。实测不是:
print(f.__code__.co_consts)
print(f.__code__.co_varnames, f.__code__.co_argcount)
print(type(f.__defaults__), len(f.__defaults__))
(None,)
('x',) 1
<class 'tuple'> 1
co_consts 里只有 None(函数体隐式的返回值),找不到 []。
默认值实际存放在函数对象的 __defaults__ 属性里,是一个 tuple;x=[] 对应 f.__defaults__[0],就是那个列表。函数被调用时,解释器把 __defaults__ 里的值填进局部变量槽位 x——填的是引用,不是拷贝。
这也解释了为什么不可变默认值(整数、字符串、None、元组)不会出问题:它们本来就没法原地修改,共享也无害。出事的只有 list / dict / set 这类可变对象。
keyword-only 参数放在另一个地方
def k(*, opt=[]):
opt.append(1)
return opt
print(k.__defaults__, k.__kwdefaults__)
print(k(), k(), k() is k())
None {'opt': [1, 1, 1, 1]}
[1, 1, 1, 1] [1, 1, 1, 1] True
* 之后的 keyword-only 默认值不进 __defaults__,而是 __kwdefaults__(一个 dict)。机制一样:函数对象上挂一份,所有调用共享。
三、求值时机:def 执行那一刻,不是调用那一刻
默认值表达式在 def 语句执行时求值一次。用一个会变的值最容易看出来:
import time
def h(t=time.time()):
return t
a, b = h(), h()
time.sleep(0.01)
c = h()
print(a, a == b, a == c)
实测:
1794110094.2900052 True True
隔了 10.9 毫秒再调,h() 拿到的还是定义时刻那个时间戳。time.time() 只跑了一次,跑在 def h(...) 被执行的时候。
这一点在做配置、缓存、随机数默认值时特别容易踩:
def make_id(prefix=uuid.uuid4().hex[:8]): # 所有调用共享同一个前缀
...
想要每次都新算,得把求值挪进函数体。
顺带理解 def 的本质:def 是可执行语句,不是编译期声明。它每次被执行都会新建一个函数对象、重新求值一次默认值:
def outer():
def inner(x=[]):
return x
return inner
a, b = outer(), outer()
print(a() is b()) # False —— 两次 def 各建了一个列表
四、运行时能换掉默认值吗?能
__defaults__ 是可写的:
f.__defaults__ = ([100],)
print(f(), f())
[100, 1, 2] [100, 1, 2]
替换之后,后续调用用的是新列表。这个能力主要用于测试 mock 和框架注入,业务代码里别用——它让函数行为取决于「谁在什么时候改过它」,比默认参数陷阱更难查。
五、同一个坑的另外两张脸
类属性里的可变对象
class Bag:
items = []
b1, b2 = Bag(), Bag()
b1.items.append('x')
print(b2.items, b1.items is b2.items)
['x'] True
根因完全一样:items 是类对象上的一个列表,实例查找时共享。正确写法是放进 __init__:
class Bag:
def __init__(self):
self.items = []
闭包里的 late binding
funcs = [lambda: i for i in range(3)]
print([g() for g in funcs]) # [2, 2, 2],不是 [0, 1, 2]
这个和默认参数不是同一机制(是闭包变量晚绑定),但常被一起当成「Python 变量作用域玄学」。解法之一恰好就是默认参数:
funcs = [lambda i=i: i for i in range(3)]
print([g() for g in funcs]) # [0, 1, 2]
这里利用的正是本文这条规则:i=i 的默认值在每次 def(lambda)执行时求值并冻结到函数对象上。
六、正确写法,以及「性能借口」不成立
标准解法是 None 哨兵:
def g(x=None):
if x is None:
x = []
x.append(1)
return x
print(g(), g(), g())
[1] [1] [1]
三次互不相同(not (g() is g()) 为 True)。None 不可变,共享无害;每次进函数体才新建列表。
有人会问:每次建列表不是更慢吗?实测一下就别拿这个当借口了:
import timeit
print(timeit.timeit(f, number=1000000)) # 共享 append
print(timeit.timeit(g, number=1000000)) # 每次建表
0.143 s
0.178 s
100 万次调用,差 0.035 秒,约合每次 35 纳秒。为了 35 纳秒换一个「调用之间互相污染」的语义 bug,怎么算都不划算。何况 f 那个版本列表会无限增长,实际场景里内存和逻辑早就先崩了。
另外提醒:if x is None 不要写成 if not x。后者会把调用方显式传进来的空列表、空字典、0、False 一并吞掉,属于另一种语义错误。
七、为什么 Python 要这么设计
不是疏忽,是几个设计决定的自然结果:
| 设计 | 后果 |
|---|---|
| 函数是一等对象 | 默认值只能是对象身上的状态,不能是「调用时临时算的局部变量」 |
def 是可执行语句 |
默认值表达式必须在 def 执行时求值,且只求一次 |
| 默认值按引用传递 | 填进局部槽位的是引用,可变对象原地改就会留痕 |
co_consts 只存不可变常量 |
可变默认值无法编译进常量池,只能挂到函数对象 |
换句话说,如果每次调用都重新求值默认值,那 def 就得在调用时重新执行一遍——函数对象也就不再是「一个确定的对象」了。CPython 选择让函数对象携带完整状态,代价就是把这条规则交给程序员记住。
八、排错清单(实测踩过的七条)
f()越调列表越长 → 可变默认值共享;改None哨兵。- 在
co_consts里找不到[]→ 它在f.__defaults__,不在字节码常量池。 - 默认值是
time.time()/uuid4()却永远不变 → 只在def执行时算一次,挪进函数体。 - keyword-only 默认值改不动
__defaults__→ 它在__kwdefaults__。 if not x吞掉了调用方传的空列表 → 用if x is None。- 类属性 list 被所有实例共享 → 移到
__init__里赋给self。 - 改了
f.__defaults__后行为诡异 → 检查是不是有框架/测试代码替换过它。
九、小结
一句话:def f(x=[]) 里的 [] 在 def 执行时创建一次,作为 __defaults__[0] 挂在函数对象上,之后每次调用都把同一个引用交给 x。
所以这个问题的正确心智模型不是「默认参数很危险」,而是「默认值是函数对象的状态」。想清楚这一点,类属性共享、闭包 late binding、__kwdefaults__、运行时替换默认值这些看起来不相干的现象,其实是同一套对象模型在不同位置的投影。
排查时也不用猜:print(f.__defaults__) 和 print(f.__code__.co_consts) 两行,就能把值到底存在哪儿钉死。
运行环境:本机 CPython 3.13.14。本文所有输出均由 d54-probe.py 实测取得,未引用外部基准数据。

浙公网安备 33010602011771号