用作默认参数的列表只创建一次,而不是每次调用都新建一个。本文演示出错时是什么样子,以及只需改一行的修复方法。
Python 的默认参数只求值一次,时机是函数定义的时候。如果默认值是列表或字典,所有调用共享的就是同一个对象。
这是 Python 里被反馈最多的意外行为。它造成的 bug 看起来就像函数记住了之前的调用。
bug 长什么样
调用三次,本意是每次都从空篮子开始。
# the bug
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item('apple'))
print(add_item('bread'))
print(add_item('milk'))
输出:
['apple']
['apple', 'bread']
['apple', 'bread', 'milk']
篮子越装越满。没人把它传进来,也从来没清空过。
为什么会这样
默认值就存放在函数对象身上,可以直接查看。
# why it happens
def add_item2(item, basket=[]):
basket.append(item)
return basket
print(add_item2.__defaults__)
add_item2('x')
print(add_item2.__defaults__)
输出:
([],)
(['x'],)
__defaults__ 里放的,正是函数每次交出去的那个列表。在函数体里追加元素,追加到的就是这个存起来的列表,下一次调用自然会看到。
修复方法
用 None 作默认值,在函数内部创建真正的值。
# the fix
def add_item3(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item3('apple'))
print(add_item3('bread'))
输出:
['apple']
['bread']
现在每次调用都会新建列表,因为函数体每次调用都会执行。
所有可变默认值都有这个问题
列表、字典和集合都是这样。数字、字符串、元组和日期这类不可变的默认值是安全的,因为没有什么能改变它们。
# it applies to every mutable default
import datetime
def log(message, seen={}, stamps=[], when=datetime.date(2020, 1, 1)):
seen[message] = True
stamps.append(when)
return len(seen), len(stamps)
print(log('first'))
print(log('second'))
输出:
(1, 1)
(2, 2)
日期默认值没问题,字典和列表有问题。
类体里的同一个陷阱
在类体里赋值的列表属于类本身,所以所有实例共享它。
# the same trap in a class body
class Basket:
items = [] # shared by every instance
a, b = Basket(), Basket()
a.items.append('apple')
print(b.items, a.items is b.items)
class BasketFixed:
def __init__(self):
self.items = []
c, d = BasketFixed(), BasketFixed()
c.items.append('apple')
print(d.items, c.items is d.items)
输出:
['apple'] True
[] False
可变属性要在 __init__ 里赋值,不要写在类体里。
要点
- 默认参数只在定义时求值一次。
- 可变默认值由所有没传这个参数的调用共享。
- 用
None作默认值,在函数内部创建真正的值。 - 类体里的可变属性也适用同样的规则。
如果在数据类(dataclass)里这样写,会抛出 ValueError。Python 很少会直接拦住你、不让问题悄悄发生,这是其中一处。