有时,使用一个空列表作为默认参数似乎很自然。然而,Python 在这些情况下会产生意想不到的行为。例如,考虑这个函数:def my_func(
有时,使用一个空列表作为默认参数似乎很自然。然而, Python 在这些情况下会产生意想不到的行为。 .
例如,考虑这个函数:
def my_func(working_list=[]):
working_list.append("a")
print(working_list)
第一次调用时,默认设置会起作用,但之后的调用将更新现有列表( "a"
每次调用一个)并打印更新后的版本。
我如何修复该函数,以便在没有明确参数的情况下重复调用它时,每次都使用一个新的空列表?
Python 会提前评估参数/参数的默认值 ;它们是“早期绑定的”。这可能会以几种不同的方式导致问题。例如:
>>> import datetime, time
>>> def what_time_is_it(dt=datetime.datetime.now()): # chosen ahead of time!
... return f'It is now {dt.strftime("%H:%M:%S")}.'
...
>>>
>>> first = what_time_is_it()
>>> time.sleep(10) # Even if time elapses...
>>> what_time_is_it() == first # the reported time is the same!
True
然而,问题最常见的表现方式是当函数的参数是 可变的 (例如, a list
)时,并在函数的代码中发生变异。 当发生这种情况时,更改将被“记住” ,从而在后续调用中“看到”:
>>> def append_one_and_return(a_list=[]):
... a_list.append(1)
... return a_list
...
>>>
>>> append_one_and_return()
[1]
>>> append_one_and_return()
[1, 1]
>>> append_one_and_return()
[1, 1, 1]
因为 a_list
是提前创建的,所以每次调用使用默认值的函数时都将使用 相同的列表对象 ,该对象在每次调用时都会被修改,并附加另一个 1
值。
这是一个 有意识的设计决策 , 可以在某些情况下加以利用 - 尽管通常有更好的方法来解决其他问题。(考虑 using functools.cache
or functools.lru_cache
进行记忆,并 functools.partial 绑定函数参数。)
这也意味着 实例的方法不能使用实例的属性作为默认值 :在确定默认值时, self
is not in scope, and the instance does not exist anyway :
>>> class Example:
... def function(self, arg=self):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in Example
NameError: name 'self' is not defined
(该类 Example
还不 存在,并且名称 Example
也不 在 范围内;因此, 即使我们不关心可变性问题,类属性在这里 也
None
标记值
标准的、普遍被认为是惯用的方法是 use None
as the default value, and explicitly check 该值并在函数的逻辑中替换它。因此:
>>> def append_one_and_return_fixed(a_list=None):
... if a_list is None:
... a_list = []
... a_list.append(1)
... return a_list
...
>>> append_one_and_return_fixed([2]) # it works consistently with an argument
[2, 1]
>>> append_one_and_return_fixed([2])
[2, 1]
>>> append_one_and_return_fixed() # and also without an argument
[1]
>>> append_one_and_return_fixed()
[1]
这种 有效,是因为 代码 a_list = []
在调用函数时 运行(如果需要) ,而不是提前运行——因此,它每次都会创建一个新的空列表。因此,这种方法也可以解决这个 datetime.now()
问题。这确实意味着函数不能将 None
值用于 其他 目的;然而,这不应该在普通代码中造成问题。
命令查询分离 的原则,不需要修改参数来实现函数的逻辑, 那么最好 不要这样做 .
By this argument, append_one_and_return
is poorly designed to begin with :由于其目的是 显示 输入的某些修改版本,因此它实际上不应该 修改 调用者的变量,而应该只创建一个 新对象 。这允许使用不可变对象(例如元组)作为默认值。因此:
def with_appended_one(a_sequence=()):
return [*a_sequence, 1]
这样,即使明确提供了输入,也可以避免修改输入:
>>> x = [1]
>>> with_appended_one(x)
[1, 1]
>>> x # not modified!
[1]
它无需参数就可以正常工作,即使反复使用也是如此:
>>> with_appended_one()
[1]
>>> with_appended_one()
[1]
并且它还获得了一些灵活性:
>>> with_appended_one('example') # a string is a sequence of its characters.
['e', 'x', 'a', 'm', 'p', 'l', 'e', 1]
PEP 671 提议为 Python 引入新语法,允许显式后期绑定参数的默认值。建议的语法是:
def append_and_show_future(a_list=>None): # note => instead of =
a_list.append(1)
print(a_list)
然而,虽然该 PEP 草案提议在 Python 3.12 中引入该功能,但这并没有实现,而且 目前还没有这样的语法可用 最近 有 关于这个想法的讨论,但它似乎不太可能在不久的将来得到 Python 的支持。