Python 可变默认参数的陷阱

FreeGuideOnline 18阅读 2026-07-06

什么是默认参数?

在定义函数时,我们可以为参数指定一个默认值。当调用者没有提供该参数时,函数就会使用这个预设的值。这让函数调用变得更灵活。

def greet(name="世界"):
    return f"你好, {name}!"

print(greet())         # 输出: 你好, 世界!
print(greet("小明"))   # 输出: 你好, 小明!

看起来一切都很完美,但当你把默认值设置为可变对象(如列表、字典、集合)时,故事就变得惊悚起来了。

可变对象与不可变对象速览

类型 可变性 例子
不可变 int, str, tuple
可变 list, dict, set

关键区别: 可变对象的内容可以原地修改,而不可变对象每次修改都会创建一个新对象。

陷阱:默认参数只初始化一次

Python 在定义函数时(而非每次调用时)就对默认参数进行求值。这意味着,如果默认值是一个可变对象,那么这个对象会被所有函数调用共享

看一个经典翻车现场:

def add_item(item, target=[]):
    target.append(item)
    return target

print(add_item(1))  # 输出: [1]
print(add_item(2))  # 预期: [2],实际输出: [1, 2]
print(add_item(3))  # 预期: [3],实际输出: [1, 2, 3]

发生了什么?

  1. 函数定义时,target=[] 创建了一个列表对象,并将其绑定到函数对象的一个属性上(add_item.__defaults__)。
  2. 第一次调用 add_item(1):没有传入 target,使用了那个唯一的列表对象,append(1) 后列表变为 [1]
  3. 第二次调用 add_item(2):依然使用同一个列表对象,此时它已经是 [1],再追加 2 就变成了 [1, 2]
  4. 这个共享的列表对象会在程序的整个生命周期里持续累积数据,除非你显式传入新的列表。

这通常不是你想要的行为,它会导致难以察觉的 bug。另一个常见例子是字典:

def set_config(key, value, config={}):
    config[key] = value
    return config

print(set_config("theme", "dark"))  # {'theme': 'dark'}
print(set_config("lang", "zh"))     # {'theme': 'dark', 'lang': 'zh'}  配置被污染了!

深入原理:__defaults____kwdefaults__

每个函数对象都有一个 __defaults__ 属性,它是一个元组,按位置顺序存储了所有具有默认值的参数值。

def buggy(a, b=[]):
    pass

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

如果修改了该列表,你可以直接在 __defaults__ 中看到变化:

buggy.__defaults__[0].append(99)
print(buggy.__defaults__)  # 输出: ([99],)

对于关键字参数,默认值存储在 __kwdefaults__ 中(一个字典)。

核心规律: 默认参数的值是存储在函数对象中的“静态”数据,而不是每次调用时动态生成的新实例。

对比不可变默认参数

如果把默认值设为不可变对象(如整数、字符串、元组),则不会出现共享副作用,因为任何“修改”操作实际上都会创建新对象,而不会改变原对象。

def safe(num=0):
    num += 1
    return num

print(safe())  # 1
print(safe())  # 1 (每次都从默认值 0 开始)

num += 1 对于整数而言是创建了一个新的整数 1,并赋值给局部变量 num,并不会修改 __defaults__ 中的 0 对象。

最佳实践:如何避开陷阱?

永远不要使用可变对象作为默认参数。使用 None 作为哨兵值,然后在函数体内部动态创建可变对象。

✅ 正确的写法:

def add_item(item, target=None):
    if target is None:
        target = []  # 每次调用都获得全新的列表
    target.append(item)
    return target

print(add_item(1))  # [1]
print(add_item(2))  # [2],完美!

对于字典:

def set_config(key, value, config=None):
    if config is None:
        config = {}
    config[key] = value
    return config

为什么这样能奏效? None 是不可变对象,每次函数调用时 target 如果没有传入参数,它就被赋值为同一个 None 对象。但我们在函数体内部立即检查并创建了新的可变对象,这就保证了每次调用都使用独立的新实例。

特殊情况:故意利用该行为

极少数情况下,你可以有意识地利用这个“静态变量”效应来实现缓存或记忆功能,但务必清晰注释,否则会让维护者感到困惑。

def memoize_factorial(n, cache={}):
    if n in cache:
        return cache[n]
    if n <= 1:
        result = 1
    else:
        result = n * memoize_factorial(n-1, cache)
    cache[n] = result
    return result

即便如此,更推荐的方法是使用 functools.lru_cache 或闭包,而不是依赖默认可变参数。

检查清单与快速记忆

  1. 默认参数仅在函数定义时计算一次。
  2. 可变对象默认值会在多次调用间共享状态。
  3. 杜绝写法: def f(x, lst=[]):def f(x, d={}):
  4. 推荐写法: def f(x, lst=None): 并在函数体内 if lst is None: lst = []
  5. 用注释说明意图,尤其是当你真的想借助这种特性时。

常见面试题速答

问:以下代码输出什么?

def magic(a, b=[]):
    b.append(a)
    return b

print(magic(1))
print(magic(2))

答: [1] 然后 [1, 2]。因为列表 b 是同一个对象,在两次调用间被追加。

问:如何修复? 答: 将默认参数改为 None,在函数内部判断 if b is None: b = []

总结

可变默认参数的陷阱是 Python 学习者几乎必踩的一个坑。理解了函数定义时的求值规则以及对象可变性的概念后,你就能轻松绕开它。记住那条黄金法则:永远用 None 代替可变对象作为默认值,并在函数体内部创建新的实例。 这样你的代码将变得可预测、无副作用,调试起来也轻松得多。