如何避免由 Python 早期绑定的默认参数引起的问题(例如可变的默认参数“记住”旧数据)?
- 2024-12-02 08:41:00
- admin 原创
- 164
问题描述:
有时,使用一个空列表作为默认参数似乎很自然。然而,Python 在这些情况下会产生意想不到的行为。
例如,考虑这个函数:
def my_func(working_list=[]):
working_list.append("a")
print(working_list)
第一次调用时,默认设置会起作用,但之后的调用将更新现有列表("a"
每次调用一个)并打印更新后的版本。
我如何修复该函数,以便在没有明确参数的情况下重复调用它时,每次都使用一个新的空列表?
解决方案 1:
def my_func(working_list=None):
if working_list is None:
working_list = []
# alternative:
# working_list = [] if working_list is None else working_list
working_list.append("a")
print(working_list)
文档说你应该使用None
它作为默认值并在函数主体中明确测试它。
除此以外
x is None
是PEP 8 推荐的比较:
与 None 等单例的比较应该始终使用
is
或来完成is not
,而永远不要使用相等运算符。
if x
另外,当你真正想写if x is not None
[...]时,要小心。
另请参阅“is None”和“== None”之间有什么区别
解决方案 2:
其他答案已经提供了所要求的直接解决方案,但是,由于这是新 Python 程序员经常犯的一个错误,因此值得补充一下 Python 为何如此表现的解释,这在《Python 漫游指南》的可变默认参数下有很好的总结:
Python 的默认参数在函数定义时只求值一次,而不是每次调用函数时都求值(就像 Ruby 中的情况一样)。这意味着,如果您使用可变默认参数并对其进行了变异,那么您也将在以后调用该函数时对该对象进行变异。
解决方案 3:
在这种情况下这并不重要,但您可以使用对象标识来测试 None :
if working_list is None: working_list = []
您还可以利用布尔运算符或 Python 中的定义方式:
working_list = working_list or []
但是,如果调用者给你一个空列表(算作假)作为 working_list,并期望你的函数修改他给出的列表,则会出现意外行为。
解决方案 4:
您的问题重点是如何修复“在没有明确参数的情况下重复调用”时的行为。您还应该问自己,在使用参数
调用时希望发生什么:
>>> CONSTANT = ['the']
>>> my_func(CONSTANT)
['the', 'a']
>>> CONSTANT # oops??
['the', 'a']
如果需要,请参阅 HenryR 的回答 ( =None
,查看is None
内部)。
但是如果你不打算改变参数,只是将其用作列表的起点,那么你可以简单地复制它:
def myFunc(starting_list=[]):
starting_list = list(starting_list)
starting_list.append("a")
print(starting_list)
(或者在这个简单的情况下,print starting_list + ["a"]
但我猜这只是一个玩具例子)
一般来说,在 Python 中改变参数是一种不好的做法。唯一可以完全改变对象的函数是对象的方法。改变可选参数的情况就更少见了——只在某些调用中发生的副作用真的是最好的接口吗?
如果您按照 C 的“输出参数”习惯来做这件事,那就完全没有必要了——您总是可以将多个值作为元组返回。
如果您这样做是为了高效地构建一长串结果列表而无需构建中间列表,请考虑将其编写为生成器并
result_list.extend(myFunc())
在调用时使用。这样您的调用约定就会保持非常清晰。
如果你觉得强制函数体不应该变异,可以选择以下防御步骤:
使用不可变默认值:
starting_list=()
。有时需要更改代码,但复制也list()
接受元组,例如return [*starting_list, "a"]
。添加类型注释并运行类型检查器。
PS经常对可选参数进行变异的一种模式是递归函数中隐藏的“备忘录”参数:
def depth_first_walk_graph(graph, node, _visited=None):
if _visited is None:
_visited = set() # create memo once in top-level call
if node in _visited:
return
_visited.add(node)
for neighbour in graph[node]:
depth_first_walk_graph(graph, neighbour, _visited)
解决方案 5:
回顾
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
值。
这是一个有意识的设计决策,可以在某些情况下加以利用- 尽管通常有更好的方法来解决其他问题。(考虑使用functools.cache
或functools.lru_cache
进行记忆,并使用functools.partial绑定函数参数。)
这也意味着实例的方法不能使用实例的属性作为默认值:在确定默认值时,self
不在范围内,并且实例不存在:
>>> 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
哨兵值
标准的、普遍被认为是惯用的方法是使用None
默认值,并明确检查该值并在函数的逻辑中替换它。因此:
>>> 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`
简单地避免可变的默认值
如果不需要为了实现函数的逻辑而修改参数,那么由于命令-查询分离的原则,最好不要这样做。
根据这个论点,append_one_and_return
一开始就设计得很糟糕:由于目的是显示输入的某些修改版本,它实际上不应该修改调用者的变量,而应该只创建一个用于显示目的的新对象。这允许使用不可变对象(例如元组)作为默认值。因此:
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
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 的支持。
解决方案 6:
我可能有点跑题了,但请记住,如果你只想传递可变数量的参数,那么 Python 式的方法是传递元组*args
或字典**kargs
。这些是可选的,比语法更好myFunc([1, 2, 3])
。
如果你想传递一个元组:
def myFunc(arg1, *args):
print args
w = []
w += args
print w
>>>myFunc(1, 2, 3, 4, 5, 6, 7)
(2, 3, 4, 5, 6, 7)
[2, 3, 4, 5, 6, 7]
如果你想传递一本字典:
def myFunc(arg1, **kargs):
print kargs
>>>myFunc(1, option1=2, option2=3)
{'option2' : 2, 'option1' : 3}
解决方案 7:
引用自https://docs.python.org/3/reference/compound_stmts.html#function-definitions
执行函数定义时,默认参数值从左到右进行求值。这意味着在定义函数时,表达式只求值一次,并且每次调用都使用相同的“预先计算”值。理解默认参数何时是可变对象(例如列表或字典)尤其重要:如果函数修改了对象(例如,通过将项目附加到列表),则默认值实际上被修改了。这通常不是预期的。解决这个问题的方法是使用 None 作为默认值,并在函数主体中明确测试它,例如:
def whats_on_the_telly(penguin=None):
if penguin is None:
penguin = []
penguin.append("property of the zoo")
return penguin
解决方案 8:
已经提供了好的和正确的答案。我只是想给出另一种语法来写你想要做的事情,当你想创建一个带有默认空列表的类时,我发现这种语法更漂亮:
class Node(object):
def __init__(self, _id, val, parents=None, children=None):
self.id = _id
self.val = val
self.parents = parents if parents is not None else []
self.children = children if children is not None else []
此代码片段使用了 if else 运算符语法。我特别喜欢它,因为它是一行简洁的语句,没有冒号等,读起来几乎像一个正常的英语句子。:)
在你的情况下你可以写
def myFunc(working_list=None):
working_list = [] if working_list is None else working_list
working_list.append("a")
print working_list
解决方案 9:
也许最简单的方法就是在脚本中创建列表或元组的副本。这样就无需检查。例如,
def my_funct(params, lst = []):
liste = lst.copy()
. .
解决方案 10:
我参加了 UCSC 的进修课程Python for programmer
以下哪项是正确的:def Fn(data = []):
a) 是一个好主意,这样您的数据列表在每次调用时都是空的。
b) 是一个好主意,这样所有对该函数的调用,如果在调用时不提供任何参数,就会得到空列表作为数据。
c) 只要您的数据是字符串列表,就是一个合理的想法。
d) 是个坏主意,因为默认的 [] 会累积数据,并且默认的 [] 会随着后续调用而改变。
回答:
d) 是个坏主意,因为默认的 [] 会累积数据,并且默认的 [] 会随着后续调用而改变。