这是 Python 初学者最容易踩、也是面试最高频问到的坑之一。先看一段看似无害的代码:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] ← 奇怪!这次调用怎么还记得 1?
print(add_item(3)) # [1, 2, 3]
你期望每次调用 add_item 时,如果不传第二个参数,就得到一个含当前元素的新列表。但实际运行结果是:默认列表在每次调用间共享了同一个对象,导致上一次追加的数据被“记住”了。
为什么会这样?
Python 函数的默认参数值只在函数定义时计算一次,而不是每次调用时重新计算。[] 是一个可变对象(列表),在定义 add_item 时,Python 创建了一个列表对象,并把它的引用绑定到形参 items 上。之后每次调用如果不传新实参,用的都是这个同一个列表对象。
所以:
- 第一次调用
add_item(1)向那个共享列表中追加了1。 - 第二次调用
add_item(2)还是向同一个列表追加2,于是列表变成了[1, 2]。
正确写法
把默认值设为 None,在函数体内判断后再创建新对象:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [2]
print(add_item(3)) # [3]
None 是不可变对象,不存在被修改的问题。每次调用时如果发现 items is None,就创建一个全新的空列表,各次调用之间互不干扰。
哪些类型容易中招?
list、dict、set等可变容器- 自定义的类实例(如果默认值是一个类实例,每次调用也可能面对同一个对象)
不可变类型(如 int、str、tuple、None、frozenset)作为默认参数通常是安全的。
一个真实场景
假设你写了一个记录日志的函数:
def log_event(event, log_list=[]):
log_list.append(event)
return log_list
如果多个地方调用它来收集事件,默认的列表会悄悄变成一个全局共享的日志缓冲区,最终所有事件混在一起,排查问题极其痛苦。正确的做法依然是 None 判断 + 内部创建。
小结
- 默认参数只在定义时求值一次,可变对象会被后续调用共享并修改。
- 永远不要用可变对象作为默认参数值,用
None替代,函数体内再创建新对象。 - 记住这个坑,可以帮你避免很多难以调试的“灵异现象”。