小张花了整整一个下午,就为了找一个bug。
他写了一个函数,用来给用户添加标签。逻辑很简单:传入用户ID和标签,把标签追加到用户的标签列表里。代码大概长这样:
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("Python"))
print(add_tag("Java"))
print(add_tag("Go"))
他预期的输出是:
['Python']
['Java']
['Go']
实际输出是:
['Python']
['Python', 'Java']
['Python', 'Java', 'Go']
小张盯着屏幕,怀疑自己是不是没睡醒。他明明每次调用都没传第二个参数,为什么列表里的内容会累积?难道Python的默认参数不是每次调用都重新创建一个空列表吗?

答案是:不是。
这个坑,几乎每个Python开发者都踩过。它藏在Python最基础的语法里,却能让老手也翻车。
先把问题复现清楚
为了看清这个问题的本质,我们把代码稍微改一下:
def add_tag(tag, tags=[]):
print(f"调用前 tags 的 id: {id(tags)}")
tags.append(tag)
print(f"调用后 tags 的内容: {tags}")
return tags
print("--- 第一次调用 ---")
add_tag("Python")
print("--- 第二次调用 ---")
add_tag("Java")
print("--- 第三次调用 ---")
add_tag("Go")
输出:
--- 第一次调用 ---
调用前 tags 的 id: 140234567890
调用后 tags 的内容: ['Python']
--- 第二次调用 ---
调用前 tags 的 id: 140234567890
调用后 tags 的内容: ['Python', 'Java']
--- 第三次调用 ---
调用前 tags 的 id: 140234567890
调用后 tags 的内容: ['Python', 'Java', 'Go']
注意那个id,三次调用完全一样。也就是说,tags这个默认参数从头到尾都是同一个列表对象。第一次调用往里面加了"Python",第二次调用时它还在,所以又加了"Java",第三次继续累积。
问题的根源在于:Python的默认参数在函数定义时就被求值了,而且只求值一次。
默认参数到底在什么时候被创建?
很多人下意识地认为,默认参数是每次调用函数时"临时"创建的。比如调用add_tag("Python")时,Python发现第二个参数没传,于是创建一个空列表[],把它赋给tags。下次调用时再创建一个新的。
但真实情况不是这样。
Python在执行def语句时,会把这个函数的默认参数值计算出来,然后保存在函数对象的一个属性里。这个属性叫__defaults__。之后每次调用函数,如果某个参数没有传值,Python就直接从__defaults__里取出那个对象来用。
我们用代码验证一下:
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag.__defaults__)
输出:
([],)
第一次调用之前,__defaults__里存的是一个空列表。调用add_tag("Python")之后,这个列表被修改了。再看一次:
add_tag("Python")
print(add_tag.__defaults__)
输出:
(['Python'],)
__defaults__里的列表已经被改变了。第二次调用时,Python拿到的就是这个已经被修改过的列表,而不是一个新的空列表。
整个过程可以这样理解:def语句执行的那一刻,Python看到了tags=[],于是创建了一个列表对象,把它存进函数的默认参数表里。之后这个列表对象就一直跟着这个函数,直到函数被销毁。每次调用不传参数,用的都是同一个列表。
这就好比你开了一家店,门口放了一个意见箱。你本来想的是"每个顾客来了都放一个新箱子",但实际上你只在开店那天放了一个箱子,之后就再也没换过。所有顾客的意见都塞进了同一个箱子里。
为什么写None就没问题?
知道了原因,解决方案就很直接了:不要用可变对象作为默认参数。
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
print(add_tag("Python"))
print(add_tag("Java"))
print(add_tag("Go"))
输出:
['Python']
['Java']
['Go']
这次对了。为什么?
因为None是一个不可变对象。函数定义时,__defaults__里存的是None。每次调用时,如果没传参数,Python取出None赋给tags。然后函数内部检查tags is None,如果是,就创建一个新的空列表。
关键的区别在于:新建列表这个动作发生在函数调用时,而不是函数定义时 。每次调用都会执行tags = [],所以每次都是一个全新的列表。
这里有一个常见的写法误区:
def add_tag(tag, tags=None):
tags = tags or []
tags.append(tag)
return tags
看起来没问题,但有个隐患:如果调用者传了一个空列表[]进来,tags or []会返回一个新的空列表,而不是使用调用者传进来的那个。调用者的列表不会被修改。这可能会导致意外的行为。所以推荐用is None判断,而不是用or。
不只列表,字典、集合都有这个问题
[]是最常见的坑,但绝不是唯一的。任何可变对象作为默认参数都有同样的问题。
字典:
def add_config(key, value, config={}):
config[key] = value
return config
print(add_config("a", 1))
print(add_config("b", 2))
输出:
{'a': 1}
{'a': 1, 'b': 2}
集合:
def add_item(item, items=set()):
items.add(item)
return items
print(add_item("a"))
print(add_item("b"))
输出:
{'a'}
{'a', 'b'}
自定义对象也一样:
class Logger:
def __init__(self):
self.logs = []
def log(message, logger=Logger()):
logger.logs.append(message)
return logger.logs
print(log("第一条"))
print(log("第二条"))
输出:
['第一条']
['第一条', '第二条']
规律是一样的:只要默认参数是可变对象,它就会被所有调用共享。
为什么Python要这样设计?
看到这里,你可能会问:Python为什么要这么设计?这不是故意坑人吗?
其实这个设计是有原因的。Python的默认参数值是在函数定义时求值的,这意味着一件很重要的事情:默认参数可以依赖函数定义时的上下文。
比如:
import datetime
def log(message, timestamp=datetime.datetime.now()):
print(f"[{timestamp}] {message}")
log("程序启动")
这里datetime.datetime.now()在函数定义时被调用了一次,之后每次调用log都用的是同一个时间戳。这可能不是你想要的效果,但它展示了默认参数的一个特性:它是在定义时确定的,不是调用时。
再比如,默认参数可以引用之前定义的变量:
DEFAULT_TIMEOUT = 30
def connect(host, timeout=DEFAULT_TIMEOUT):
pass
如果默认参数在每次调用时才求值,那么DEFAULT_TIMEOUT如果在之后被修改,connect的默认行为也会跟着变。但因为在定义时求值,函数的行为就固定下来了。
Python选择"定义时求值"而不是"调用时求值",是为了让函数的行为更可预测。但代价就是,可变默认参数会被共享。
这个设计本身没有错,错的是我们使用它的方式。可变对象作为默认参数,几乎从来都不是我们想要的行为。
这个坑为什么这么难发现?
这个问题的隐蔽性在于:它在简单场景下不会暴露。
如果你只调用一次函数,完全没问题。如果你每次调用都传了第二个参数,也没问题。只有当你不传参数、并且多次调用时,问题才会显现。
更麻烦的是,它可能不会立刻出错。数据会静默地累积,直到某个时刻才引发异常。比如你写了一个处理请求的函数,默认参数是个字典,用来存缓存。你以为每次请求都是独立的,结果上一个请求的数据泄漏到了下一个请求里。这种bug在测试环境可能永远发现不了,一到生产环境就出问题。
还有一个让人困惑的地方:默认参数在函数定义时只求值一次这条规则,很多人其实"知道",但没有真正理解。考试能答对,写代码时照样踩坑。因为直觉上,"默认值"听起来就像是每次调用时才会用到的东西。
怎么彻底避免这个问题?
最根本的原则只有一条:永远不要用可变对象作为默认参数。
具体来说,以下这些都不能作为默认参数:
-
[] -
{} -
set() -
bytearray() -
任何自定义的可变对象
-
任何包含可变对象的容器
正确的做法是:默认值用None,在函数体内判断并创建。
def process(items=None):
if items is None:
items = []
# 继续处理
这个模式适用于所有情况。
如果你觉得每次都写if xxx is None太麻烦,可以用一个辅助函数:
def ensure_list(value):
return [] if value is None else value
def add_tag(tag, tags=None):
tags = ensure_list(tags)
tags.append(tag)
return tags
但说实话,多写两行代码换来的是代码行为的确定性,这笔买卖很划算。
有没有故意用可变默认参数的情况?
有,但很少,而且通常不值得。
有些人利用可变默认参数来实现缓存:
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(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
还有人用可变默认参数来记录函数被调用的历史:
def track_call(arg, history=[]):
history.append(arg)
return history
但这种"隐藏状态"让函数的行为变得不可预测,调试起来很痛苦。不如显式传一个列表进来,或者用类来管理状态。
在绝大多数情况下,可变默认参数带来的便利远远小于它带来的风险。所以业界共识就是:别用。
一个容易忽略的变体:类属性
和默认参数类似,类属性也有共享的问题:
class User:
tags = []
def add_tag(self, tag):
self.tags.append(tag)
u1 = User()
u2 = User()
u1.add_tag("Python")
print(u2.tags)
输出:
['Python']
u2的tags里居然有u1添加的标签。因为tags = []是类属性,所有实例共享同一个列表。解决方案是在__init__里初始化:
class User:
def __init__(self):
self.tags = []
这个坑和默认参数的坑本质上是同一个问题:可变对象被多个上下文共享。
再回到小张的故事
小张最后把代码改成了:
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
问题解决了。他顺手查了一下项目里还有没有类似的写法,结果发现了七八处。有的是默认参数用了{},有的是类属性用了[],还有一处是默认参数用了datetime.now(),导致所有日志的时间戳都一样。
他花了一个小时全部改完,然后在团队群里发了一条消息:"以后写默认参数,别用[],用None。别问我怎么知道的。"
这个坑不会报错,不会崩溃,只会在某个不经意的时刻让你怀疑人生。而避免它的成本,只是多写一行if xxx is None。
Python的默认参数在定义时求值,而且只求值一次。可变对象一旦被共享,所有调用都会互相影响。写[],你得到的是一个共享的列表;写None,你得到的是每次调用都新建的列表。
区别就是这么大。