← 返回题目列表

Python 为什么不建议使用可变对象作为函数默认参数?

高频 中等 第 10 / 21 题 更新于 2026/07/27
默认参数可变对象函数定义None 哨兵

简化版

因为 Python 的默认参数在函数定义时求值一次,不是每次调用都重新创建。把列表、字典、集合这类可变对象作为默认参数,会导致多次调用共享同一个对象,产生意外状态残留。

详细版

典型错误写法:

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

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b']

很多人期待第二次调用返回 ['b'],但实际返回 ['a', 'b']。原因是 items=[] 只在函数定义时执行一次,之后每次没有传 items 时都会复用同一个列表。

推荐写法是使用 None 作为哨兵值:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

这样每次没有传入列表时,函数内部都会创建一个新的列表。

需要注意:默认参数不是不能用,而是不要用“会被修改的可变对象”做默认值。不可变对象如 None、数字、字符串、元组通常没问题。

完整版教学

一、默认参数的求值时机

Python 函数定义语句执行时,会创建函数对象,同时计算默认参数表达式。

def f(x=[]):
    return x

执行到这段代码时,[] 会被创建一次,并保存在函数对象的默认参数里。之后每次调用 f(),如果调用者没有传 x,Python 就取出这同一个列表对象作为参数。

可以用 __defaults__ 观察:

def f(x=[]):
    x.append(1)
    return x

print(f.__defaults__)  # ([],)
f()
print(f.__defaults__)  # ([1],)

默认列表本身被修改了,所以函数对象里的默认值也变了。

二、为什么这个设计存在

这不是 Python 的 bug,而是语言设计的一部分。默认值在定义时求值,有两个好处:

  • 不需要每次调用都重新计算默认表达式;
  • 默认值可以捕获定义时的对象或配置。

例如:

import time

def created_at(ts=time.time()):
    return ts

这里 ts 是函数定义时的时间,不是每次调用的时间。它可能不是你想要的行为,但它是符合规则的。

同理:

def connect(timeout=3):
    ...

3 是不可变整数,不会被修改,所以没有风险。

三、可变默认参数的典型坑

最常见的是列表累积:

def collect(value, bucket=[]):
    bucket.append(value)
    return bucket

还有字典缓存误用:

def set_option(key, value, options={}):
    options[key] = value
    return options

第二个函数每次调用都会污染同一个 options 字典。

这类问题难排查,因为单次调用看起来正常,多次调用或单元测试顺序变化时才会暴露。

四、标准写法:None 哨兵

更稳的写法是:

def collect(value, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(value)
    return bucket

为什么用 is None,而不是 if not bucket

因为空列表、空字典、空字符串都是假值。调用者可能明确传入一个空列表,函数应该尊重这个参数,而不是误以为没有传。

def f(items=None):
    if items is None:
        items = []

这个判断表达的是“参数缺省”,不是“参数内容为空”。

五、什么时候可变默认参数反而能用

少数情况下,开发者会有意利用这个特性做缓存:

def fib(n, cache={0: 0, 1: 1}):
    if n not in cache:
        cache[n] = fib(n - 1) + fib(n - 2)
    return cache[n]

但面试和工程代码里不建议把它作为常规写法。更清晰的方式是使用 functools.lru_cache

from functools import lru_cache

@lru_cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

显式工具比隐藏在默认参数里的状态更容易读懂,也更容易维护。

六、常见误区与追问

易错点:默认参数挂在函数对象上,而不是挂在“某一次调用”上。看到 def f(x=[]),要把它想成函数对象里长期保存着同一个列表。

  • 误区:默认参数会在每次调用函数时重新创建。 Python 是在执行 def 语句、创建函数对象时计算默认值;后续调用缺省参数时复用这个对象,所以列表、字典、集合会留下历史修改。
  • 追问:为什么 None 哨兵要用 is None 判断? 因为 None 是单例,is 表达身份判断;同时空列表、空字典、0、空字符串都是假值,用 if not items 会把调用者明确传入的空容器误判成“没传”。
  • 误区:所有默认参数都危险。 不可变对象如整数、字符串、None 通常安全;真正危险的是默认对象会被原地修改,例如 appendupdateadd
  • 追问:默认参数里写 time.time() 会发生什么? 它会在函数定义时执行一次,而不是每次调用都更新时间;如果需要调用时刻,应把默认值设为 None,在函数体内调用 time.time()
  • 误区:可变默认参数永远不能出现。 它可以被刻意用作隐藏缓存,但这会把状态藏在函数签名里,工程上通常不如 lru_cache、实例属性或显式缓存对象清楚。
  • 追问:怎么在排查线上问题时确认是不是默认参数共享? 可以打印 func.__defaults__ 或连续调用两次最小复现;如果第二次结果带着第一次的数据,基本就是默认可变对象被复用了。

七、加强记忆

默认参数不是“调用时创建”,而是“定义时创建”。不可变默认值通常安全,可变默认值一旦被修改,就会在多次调用之间共享状态。写函数时先问自己:这个默认对象会不会被改?如果会,就用 None 做哨兵,在函数内部创建新对象。