被Python默认参数坑了三次:空列表当默认值,数据居然会"传染"

这个坑我前后踩过三次,每次都以为记住了,换个场景又翻车。最离谱的一次,Python 生产环境接口返回的数据"带记忆",排查了两小时才发现是函数默认参数在搞鬼——把空列表当默认值,结果数据在函数调用之间“传染”。写出来,希望能帮你省下这两小时。


场景还原:一个看似无害的工具函数

写了个小工具,用来收集用户的操作日志,默认攒到一个列表里,最后统一落库。代码长这样:

from datetime import datetime

def collect_logs(action, logs=[]):
    logs.append(f"[{datetime.now()}] {action}")
    return logs

逻辑很直白:传了 logs 就用传的,没传就默认给个空列表,然后把新动作追加上去。

测试的时候没问题:

>>> collect_logs("用户登录")
['[2026-05-18 09:12:33] 用户登录']

>>> collect_logs("查看报表")
['[2026-05-18 09:12:33] 用户登录', '[2026-05-18 09:12:35] 查看报表']

等等,第二次调用我没传 logs,怎么把第一次的日志也带出来了?

当时还以为是终端显示问题,或者自己眼花了。清空屏幕再试一遍,结果更魔幻:

>>> collect_logs("导出数据")
['[2026-05-18 09:12:33] 用户登录', '[2026-05-18 09:12:35] 查看报表', '[2026-05-18 09:12:40] 导出数据']

这函数有记忆?同一个进程里,每次调用默认参数都在累加。直觉告诉我,logs=[] 这个地方绝对有问题。
下面把原因说清楚,顺便给出彻底的解决办法。


根本原因:Python 的默认参数在定义时求值,且只求一次

大部分语言里,默认参数每次调用都会重新初始化。但 Python 不是。

Python 的默认参数在 函数定义的时候求值一次,之后每次调用如果省略这个参数,用的都是同一个对象。列表又是可变对象,append 操作直接改的是这个"老对象"本身,而不是创建新列表。

换句话说,logs=[] 里的那个空列表,是函数对象的属性,挂在 func.__defaults__ 上。验证一下:

from datetime import datetime

def collect_logs(action, logs=[]):
    logs.append(f"[{datetime.now()}] {action}")
    return logs

print(collect_logs.__defaults__)
# 输出: ([],)

collect_logs("登录")
collect_logs("登出")

print(collect_logs.__defaults__)
# 输出: (['[...] 登录', '[...] 登出'],)

看到没,__defaults__ 里的那个列表,已经被改掉了。所有没传 logs 的调用,共享的就是这同一个列表。

这个设计不是 bug,是 Python 的一个语言特性,但确实反直觉。Python 官方 FAQ 里有专门一节讲这个事(“Why did changing list x also change list y?”),但日常开发谁去看 FAQ,踩了才知道疼。


完整解决方案

正确写法:用 None 做占位符,函数内部初始化

from datetime import datetime

def collect_logs(action, logs=None):
    if logs is None:
        logs = []
    logs.append(f"[{datetime.now()}] {action}")
    return logs

这样每次调用不传 logs 时,都会执行 logs = [],创建的是全新的列表对象,互不干扰。

验证:

>>> collect_logs("用户登录")
['[2026-05-18 09:15:10] 用户登录']

>>> collect_logs("查看报表")
['[2026-05-18 09:15:12] 查看报表']

>>> collect_logs("导出数据")
['[2026-05-18 09:15:15] 导出数据']

各自独立,没问题了。


举一反三:其他可变对象也一样

不只是列表,所有可变类型当默认参数都有这问题。常见踩坑对象:

类型 错误写法 正确写法
列表 def f(a=[]) def f(a=None): if a is None: a = []
字典 def f(a={}) def f(a=None): if a is None: a = {}
集合 def f(a=set()) def f(a=None): if a is None: a = set()

不可变类型(字符串、数字、元组)没有这个问题,因为它们不能被原地修改,每次操作都会生成新对象。


避坑总结

  • 永远不要把可变对象(列表、字典、集合)直接写在函数默认参数里,这是 Python 最经典的坑之一。
  • None 做占位符是标准做法,在函数体内部判断后再初始化。
  • 不要写 if not logs 来判断,如果用户传了个空列表 []if not logs 也会进初始化分支,把人家传的参数给覆盖了。举个例子:写个合并配置的函数,用户传了空字典 {} 想做增量更新,结果被你重置成新 {},之前传入的其他字段全丢了。必须用 if logs is None
  • 如果你在看同事代码时看到 def xxx(arg=[])顺手改了吧,这颗雷迟早会炸。

这坑不大,但隐蔽。三次踩坑经历告诉我:越是看上去人畜无害的语法糖,越要小心。你踩过这个坑吗?或者踩过更离谱的?评论区说说,大家一起避。

更多推荐