定义函数时给参数一个默认值,调用时就能省去传参的麻烦,这是Python里非常贴心的特性。但不少人在用了这个特性一段时间后,会发现函数的行为突然变得诡异:明明没传参数,返回结果却和上次调用有关;列表参数越用越长;日志越打越多。这些问题几乎都指向同一个根源——对缺省参数的求值机制理解不到位。本文把缺省参数的核心原理和常见陷阱梳理一遍,帮你彻底搞清楚它的正确用法。

缺省参数的求值时机:只执行一次
先明确一个关键结论:Python中缺省参数的默认值,在函数被定义的那一刻就求值了,而不是每次调用时重新求值。这一点和其他一些语言不同,也是绝大多数坑的来源。看下面这段代码:
import time
def report_timestamp(ts=time.time()):
print("时间戳:", ts)
report_timestamp()
time.sleep(2)
report_timestamp()
两次调用打印出来的时间戳完全一样。因为time.time()只在def语句执行时运行了一次,之后这个值就被固定下来存在函数对象里了。你可以通过函数的__defaults__属性验证这一点,它保存的就是那份求值好的默认值。
理解了这个机制,很多看似灵异的现象就都能解释了。默认值本质上是一个和函数绑定在一起的对象,所有调用共享同一份。如果这个对象是不可变的,比如数字、字符串、元组,共享没有任何副作用;但如果它是可变的,问题就来了。
最经典的坑:可变对象作为默认值
列表和字典是可变对象,把她们设为默认值时,函数内部对默认对象的修改会一直保留,影响到后续调用。看这个例子:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # 输出 [1]
print(add_item(2)) # 输出 [1, 2],而不是预期的 [2]
print(add_item(3)) # 输出 [1, 2, 3]
第二次调用时并没有传入items,函数用的还是第一次那个列表对象,所以2被追加到了[1]后面。这种bug特别隐蔽,开发阶段测试单个用例往往一切正常,等程序跑起来多次调用后才暴露,而且报错信息通常不明显,排查起来很费时间。
再看一个字典版本,常出现在收集配置或缓存的场景里:
def put_cache(key, value, cache={}):
cache[key] = value
return cache
put_cache("a", 1)
put_cache("b", 2)
print(put_cache("c", 3)) # {'a': 1, 'b': 2, 'c': 3}
整个进程里所有调用共享同一个字典,等于变相造了一个全局变量。如果确实需要跨调用保留状态,那应该明确写成全局变量或者类的属性,而不是依赖这种隐式行为。顺便一提,flake8等静态检查工具会对可变默认值给出警告(常见的B006规则),写代码时开启这类检查能提前拦住问题。
正确写法:用None作哨兵值
标准解决办法是把默认值设为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],符合预期
这里注意要用is None做判断,而不是if not items。因为如果调用方传入一个空列表、空字符串或者0,not items也会为真,会把调用方特意传进来的对象悄悄替换掉,造成另一种更难察觉的错误。
如果None本身可能是合法的传入值,可以自定义一个哨兵对象:
_missing = object()
def process(data=_missing):
if data is _missing:
data = []
return data
由于object()每次调用都会生成一个独一无二的新对象,外界不可能构造出和它相等的值,所以判断绝对不会误伤。
参数顺序与混用规则
除了默认值本身,参数排列顺序也有硬性规定:带默认值的参数必须放在不带默认值的参数后面。也就是说,def f(a, b=1)合法,而def f(a=1, b)会直接抛出语法错误。原因很直观,位置参数是按顺序匹配的,如果默认参数排在前面,解释器无法确定后面的实参该给谁。
当缺省参数和*args、**kwargs混用时,顺序是:普通参数、缺省参数、*args、仅关键字参数、**kwargs。一个容易出错的地方是给*args后面的参数传值,必须用关键字形式:
def connect(host, port=3306, *args, timeout=10, **kwargs):
print(host, port, timeout)
connect("127.0.0.1", 3307, "extra", timeout=30)
这里的timeout就是仅关键字参数,不写timeout=30而直接按位置传会报错。这种写法在标准库和主流框架里非常常见,既能提供默认值,又能避免调用方误传位置参数。
总结
缺省参数的核心记忆点就一条:默认值在函数定义时求值一次,所有调用共享。由此推导出三条实践准则:第一,永远不要用列表、字典、集合等可变对象做默认值,统一改成None加函数体内判断的写法;第二,判断时用is None而不是真值测试,避免误伤空对象;第三,注意参数顺序规则,缺省参数必须在位置参数之后。把这些习惯固化下来,缺省参数这个便利特性才能真正为你所用,而不是埋雷。
Python缺省参数默认参数可变对象修改时间:2026-09-07 15:08:36