因为默认参数在函数定义时初始化且复用同一对象,可变对象(如列表)会被原地修改而“记忆”历史状态;安全做法是用None占位并在函数内初始化。

为什么 def func(x=[]): 会“记住”上次调用的修改
因为默认参数在函数定义时就完成了初始化,不是每次调用才新建。这个 [] 对象在内存里只创建一次,后续所有没传参的调用都共用它。
常见错误现象:func() 第一次返回 [1],第二次调用却返回 [1, 1],第三次变成 [1, 1, 1]——列表像被“累加”了。
- 本质是 python 的函数对象把默认参数作为其
__defaults__属性的一部分,属于函数本身,不是调用栈 - 所有可变对象都这样:列表、字典、集合、自定义类实例……只要它能被原地修改(
.append()、.update()等)就会出问题 - 不可变对象(如
int、str、tuple)不会表现出这种“记忆”,但也不建议依赖——语义上它们本就不该被“修改”
怎么安全地写带默认空容器的函数
标准解法是用 None 占位,然后在函数体内手动初始化。
def append_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst
- 永远不要把
list、dict等直接写在def的参数默认值里 - 检查是否为
None,而不是用if not lst:——空列表也是False,会误触发重建 - 如果想支持用户显式传入
None,可用哨兵对象:_sentinel = Object(),再判断lst is _sentinel
哪些场景最容易踩这个坑
高频出问题的地方往往藏着“看起来无害”的初始化逻辑。
立即学习“Python免费学习笔记(深入)”;
- 缓存类函数:
def get_config(cache={})—— 多次调用后cache键越来越多 - 递归辅助函数:
def dfs(node, path=[])—— 不同分支的路径互相污染 - django/flask 视图中传表单初始数据:
def view(request, errors=[])—— 错误消息跨请求残留 - 用
functools.lru_cache装饰器时,如果被装饰函数用了可变默认参数,缓存键可能出错或失效
Python 3.8+ 有新变化吗
没有。行为完全一致。PEP 570(positional-only 参数)和 PEP 622(match-case)都不影响默认参数求值时机。
有人误以为 typing.Optional 或 dataclass 的 default_factory 是对这个问题的“修复”,其实不是——default_factory 是专门为此设计的机制,而普通函数参数没这层抽象。
真正容易被忽略的是:类方法里的默认参数同样适用这条规则,比如 def method(self, items=[]),每个实例调用都共享同一个 items 列表。