人人都会AI编程

30.1 可变默认参数陷阱

更新时间:2026-07-12

这是 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,就创建一个全新的空列表,各次调用之间互不干扰。

哪些类型容易中招?

  • listdictset 等可变容器
  • 自定义的类实例(如果默认值是一个类实例,每次调用也可能面对同一个对象)

不可变类型(如 intstrtupleNonefrozenset)作为默认参数通常是安全的。

一个真实场景

假设你写了一个记录日志的函数:

def log_event(event, log_list=[]):
    log_list.append(event)
    return log_list

如果多个地方调用它来收集事件,默认的列表会悄悄变成一个全局共享的日志缓冲区,最终所有事件混在一起,排查问题极其痛苦。正确的做法依然是 None 判断 + 内部创建。

小结

  • 默认参数只在定义时求值一次,可变对象会被后续调用共享并修改。
  • 永远不要用可变对象作为默认参数值,用 None 替代,函数体内再创建新对象。
  • 记住这个坑,可以帮你避免很多难以调试的“灵异现象”。