Python中的可变默认参数问题解析

Python中的可变默认参数问题解析

技术背景

在Python编程中,当我们定义函数时可以为参数设置默认值。然而,当默认参数是可变对象(如列表、字典等)时,可能会出现与预期不符的行为,这被称为“最少惊讶原则”的违背。例如:

1
2
3
4
5
6
def foo(a=[]):
a.append(5)
return a

print(foo()) # 输出: [5]
print(foo()) # 输出: [5, 5]

Python新手可能期望每次调用foo()函数时都返回一个只包含元素5的列表,但实际结果却并非如此。这种行为引发了对Python语言设计的讨论,为什么默认参数是在函数定义时绑定,而不是在函数执行时绑定呢?

实现步骤

1. 理解默认参数的绑定时间

在Python中,函数定义是一个执行过程,当Python解释器遇到函数定义语句时,会创建函数对象,并对默认参数进行求值。这意味着默认参数的值在函数定义时就已经确定,并且在后续函数调用时不会重新求值。

2. 观察可变对象的影响

由于默认参数在函数定义时就已确定,并且可变对象可以在原地修改,因此每次调用函数时,如果使用默认参数,实际上使用的是同一个可变对象的引用。这就导致了上述例子中列表不断增长的现象。

3. 验证默认参数的存储位置

可以通过函数的__defaults__属性来验证默认参数的存储情况:

1
2
3
4
5
6
def func(a = []):
a.append(5)

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

从上述代码可以看出,默认参数的值存储在函数的__defaults__属性中,并且在函数调用后,该属性的值会发生变化。

核心代码

避免可变默认参数问题的常见做法

为了避免可变默认参数带来的问题,通常使用None作为默认参数,然后在函数内部进行判断和初始化:

1
2
3
4
5
6
7
8
def whats_on_the_telly(penguin=None):
if penguin is None:
penguin = []
penguin.append("property of the zoo")
return penguin

print(whats_on_the_telly()) # 输出: ['property of the zoo']
print(whats_on_the_telly()) # 输出: ['property of the zoo']

使用装饰器解决问题

可以编写一个装饰器来自动处理可变默认参数的复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import inspect
from copy import deepcopy

def sanify(function):
def wrapper(*a, **kw):
defaults = inspect.getargspec(function).defaults # for python2
new_args = []
for i, arg in enumerate(defaults):
if i in range(len(a)):
new_args.append(a[i])
else:
new_args.append(deepcopy(arg))
return function(*new_args, **kw)
return wrapper

@sanify
def foo(a=[]):
a.append(5)
return a

print(foo()) # 输出: [5]
print(foo()) # 输出: [5]

最佳实践

谨慎使用可变默认参数

除非有明确的需求,否则应尽量避免使用可变对象作为默认参数。如果确实需要使用可变对象,可以使用上述的None判断方法来确保每次调用函数时都使用新的对象。

明确函数的行为

在编写函数时,应明确函数是否会修改传入的参数。如果会修改,应在函数文档中明确说明,以避免调用者产生误解。

常见问题

为什么Python要在函数定义时绑定默认参数?

  • 一致性:函数定义是一个执行过程,默认参数作为函数定义的一部分,在定义时求值符合Python的执行模型。如果在函数调用时求值,会导致def语句的行为不一致,部分绑定在定义时发生,部分绑定在调用时发生。
  • 性能优化:对于一些创建成本较高的对象,如果每次调用函数都重新创建,会影响性能。在函数定义时创建并绑定默认参数,可以避免重复创建。
  • 内省功能:在函数定义时确定默认参数的值,方便通过inspect模块进行内省,获取函数的参数信息和默认值。

如何判断一个函数是否使用了可变默认参数?

可以通过检查函数的__defaults__属性来判断是否包含可变对象:

1
2
3
4
def func(a=[]):
pass

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

如果__defaults__属性中包含可变对象,则说明该函数使用了可变默认参数。


Python中的可变默认参数问题解析
https://119291.xyz/posts/2025-04-21.python-mutable-default-argument-analysis/
作者
ww
发布于
2025年4月22日
许可协议