# Python默认参数为什么不能随便使用列表和字典?
面试官问:"下面这段代码有什么问题?"
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("Python")) # ['Python']
print(add_item("Go")) # ['Python', 'Go']
2
3
4
5
6
第二次调用返回['Python', 'Go']而不是['Go'],多数人第一次遇到时都会卡住。问题的根源在于:默认参数在函数定义时创建,所有调用共享同一个对象。
# 简要回答
Python默认参数在函数定义时求值并创建对象,存储在函数对象的属性中。 每次调用时,如果不传递该参数,形参就绑定到这个已经存在的默认对象。
对于可变对象(列表、字典、集合),多次调用会修改同一个对象,导致前一次调用的副作用累积到下一次调用。
正确做法是用None作为默认值,函数内判断并创建新对象:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
2
3
4
5
# 详细回答
# 默认参数何时创建
函数定义时,Python解释器执行def语句,会:
- 创建函数对象。
- 计算默认参数表达式的值,创建默认对象。
- 把默认对象存储在函数对象的
__defaults__属性中。
def add_item(item, items=[]):
items.append(item)
return items
print(add_item.__defaults__) # ([],)
2
3
4
5
__defaults__是一个元组,包含所有默认参数对象。这个元组在函数定义时创建,之后不会改变。
# 多次调用共享同一个对象
def add_item(item, items=[]):
items.append(item)
return items
list1 = add_item("Python")
list2 = add_item("Go")
list3 = add_item("C++")
print(list1) # ['Python', 'Go', 'C++']
print(list2) # ['Python', 'Go', 'C++']
print(list3) # ['Python', 'Go', 'C++']
print(list1 is list2) # True,三个名字指向同一个列表
2
3
4
5
6
7
8
9
10
11
12
每次调用时,如果不传递items参数,形参items就绑定到__defaults__[0]指向的那个列表。三次调用都修改同一个列表,所以list1、list2、list3实际上是同一个对象。

# 正确的写法
用None作为哨兵值,函数内判断并创建新对象:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("Python")) # ['Python']
print(add_item("Go")) # ['Go']
2
3
4
5
6
7
8
每次调用时,如果items是None,就创建一个新列表。不同调用之间不再共享对象。
# 默认字典同理
def add_score(name, score, scores={}):
scores[name] = score
return scores
print(add_score("Alice", 90)) # {'Alice': 90}
print(add_score("Bob", 85)) # {'Alice': 90, 'Bob': 85}
2
3
4
5
6
两次调用共享同一个字典,第二次调用看到了第一次的副作用。
正确写法:
def add_score(name, score, scores=None):
if scores is None:
scores = {}
scores[name] = score
return scores
2
3
4
5
# 不可变对象作为默认值没有问题
def greet(name, prefix="Hello"):
return f"{prefix}, {name}!"
print(greet("Alice")) # Hello, Alice!
print(greet("Bob", "Hi")) # Hi, Bob!
2
3
4
5
字符串、整数、元组等不可变对象不能被修改,每次操作都会创建新对象,不会累积副作用。
# 利用默认参数实现缓存
有些场景下,共享可变默认参数是故意的:
def fibonacci(n, cache={}):
if n in cache:
return cache[n]
if n <= 1:
return n
cache[n] = fibonacci(n - 1) + fibonacci(n - 2)
return cache[n]
print(fibonacci(10)) # 55
print(fibonacci.__defaults__) # ({0: 0, 1: 1, 2: 1, ...},)
2
3
4
5
6
7
8
9
10
cache在所有调用之间共享,避免重复计算。但这种写法容易让阅读代码的人困惑,更推荐用functools.lru_cache装饰器或全局变量。
# 知识拓展
Q1:为什么Python要在定义时求值默认参数?
设计决策。定义时求值让默认参数可以是任意表达式,包括函数调用、类实例化等。如果改成调用时求值,会增加运行时开销,且让默认参数的行为更难预测。
Q2:如何查看函数的默认参数?
访问__defaults__属性:
def example(a, b=10, c=[]):
pass
print(example.__defaults__) # (10, [])
2
3
4
关键字参数的默认值在__kwdefaults__中。
Q3:传递参数时会覆盖默认值吗?
会。传递参数后,形参绑定到传入的对象,不再使用默认值:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
my_list = ["existing"]
result = add_item("new", my_list)
print(result) # ['existing', 'new']
print(my_list) # ['existing', 'new'],外部列表被修改
2
3
4
5
6
7
8
9
10
Q4:能在定义时就创建多个独立的默认对象吗?
不能。def只执行一次,默认参数对象只创建一次。想要每次调用都得到新对象,只能在函数内创建。
Q5:PyLint或Flake8会检测这个问题吗?
会。静态检查工具会对可变默认参数发出警告(W0102 / dangerous-default-value)。IDE通常也会高亮提示。
评论
验证登录状态...