Blog

Python 可变默认参数的陷阱

用作默认参数的列表只创建一次,而不是每次调用都新建一个。本文演示出错时是什么样子,以及只需改一行的修复方法。

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 很少会直接拦住你、不让问题悄悄发生,这是其中一处。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。