“Least Astonishment”和可变的默认参数

"Least Astonishment" and the Mutable Default Argument

提问人:Stefano Borini 提问时间:7/16/2009 最后编辑:Volker SiegelStefano Borini 更新时间:8/12/2023 访问量:259843

问:

任何对 Python 进行足够长时间修补的人都被以下问题咬了(或撕成碎片):

def foo(a=[]):
    a.append(5)
    return a

Python 新手希望这个不带参数的函数调用始终返回一个只有一个元素的列表:.相反,结果非常不同,而且非常令人惊讶(对于新手):[5]

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

我的一位经理曾经第一次遇到这个功能,并称其为语言的“戏剧性设计缺陷”。我回答说,这种行为有一个潜在的解释,如果你不了解内部结构,它确实非常令人费解和出乎意料。但是,我无法回答(我自己)以下问题:在函数定义而不是在函数执行时绑定默认参数的原因是什么?我怀疑有经验的行为是否有实际用途(谁真的在 C 中使用了静态变量,而没有滋生错误?

编辑

巴切克举了一个有趣的例子。连同您的大部分评论,尤其是 Utaal 的评论,我进一步阐述了:

def a():
    print("a executed")
    return []

           
def b(x=a()):
    x.append(5)
    print(x)

a executed
>>> b()
[5]
>>> b()
[5, 5]

在我看来,设计决策似乎是相对于将参数范围放在哪里有关:在函数内部,还是与函数“一起”?

在函数内部执行绑定意味着在调用函数时,该函数被有效地绑定到指定的默认值,而不是定义,这将带来一个严重的缺陷:从某种意义上说,该行将是“混合”的,因为(函数对象的)绑定的一部分将在定义时发生,而部分(默认参数的分配)将在函数调用时发生。xdef

实际行为更加一致:当执行该行时,该行的所有内容都会被计算,这意味着在函数定义时。

python 语言设计 默认参数 最小惊讶

评论

81赞 Jonathan Livni 2/7/2012
补充问题 - 可变默认参数的良好用途
9赞 Serge 4/7/2017
对于一个普通人来说,我毫不怀疑可变的论点违反了最小惊讶原则,我见过初学者走到那里,然后英勇地用邮件元组替换邮件列表。尽管如此,可变参数仍然符合 Python Zen (Pep 20),并且属于“荷兰语显而易见”(被硬核 python 程序员理解/利用)子句。推荐的文档字符串解决方法是最好的,但对文档字符串和任何(书面)文档的抵制现在并不少见。就个人而言,我更喜欢装饰器(比如@fixed_defaults)。
6赞 Alan Leuthard 6/2/2017
当我遇到这种情况时,我的论点是:“为什么你需要创建一个函数来返回一个可变对象,这个可变对象可以选择是你传递给函数的可变对象?它要么改变可变对象,要么创建一个新的可变对象。为什么需要用一个函数同时执行这两个操作?为什么要重写解释器,让你在不增加三行代码的情况下做到这一点?因为我们在这里谈论的是重写解释器处理函数定义和调用的方式。对于一个几乎不需要的用例来说,这是很多事情要做。
32赞 symplectomorphic 7/7/2017
“Python 新手希望这个函数总是返回一个只有一个元素的列表:.”我是 Python 新手,我没想到会这样,因为显然会返回 ,而不是.你的意思是,新手会期望没有参数调用的函数总是返回。[5]foo([1])[1, 5][5][5]
7赞 smci 4/21/2019
这个问题问:“为什么这个[错误的方式]会如此实施?它不会问“什么是正确的方法?”,这在[为什么使用arg=None可以解决Python的可变默认参数问题?*(stackoverflow.com/questions/10676729/...)。新用户几乎总是对前者不太感兴趣,而对后者更感兴趣,因此这有时是一个非常有用的链接/复制品。

答:

336赞 Eli Courtwright 7/16/2009 #1

假设您有以下代码

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

当我看到 eat 的声明时,最不令人惊讶的是认为,如果没有给出第一个参数,它将等于元组("apples", "bananas", "loganberries")

但是,假设稍后在代码中,我执行类似操作

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

然后,如果默认参数是在函数执行时绑定的,而不是在函数声明时绑定的,我会惊讶地(以一种非常糟糕的方式)发现结果已经改变。这比发现您上面的函数正在改变列表更令人惊讶。foo

真正的问题在于可变变量,所有语言在某种程度上都存在这个问题。这里有一个问题:假设在 Java 中我有以下代码:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

现在,我的映射是使用键放入映射时的值,还是通过引用存储键?无论哪种方式,都会有人感到惊讶;要么是试图使用与他们放入对象相同的值将对象从中取出的人,要么是似乎无法检索对象的人,即使他们使用的键实际上与用于将其放入映射中的对象相同(这实际上就是 Python 不允许将其可变内置数据类型用作字典键的原因)。StringBufferMap

你的例子是一个很好的例子,Python 新手会感到惊讶和咬伤。但我认为,如果我们“修复”了这一点,那么这只会造成一种不同的情况,即它们会被咬伤,而且这种情况会更不直观。此外,在处理可变变量时总是如此;你总是会遇到这样的情况,即有人可以根据他们正在编写的代码直观地期望一种或相反的行为。

我个人喜欢 Python 当前的方法:默认函数参数在定义函数时计算,并且该对象始终是默认值。我想他们可以使用空列表进行特殊情况,但这种特殊大小写会引起更大的惊讶,更不用说向后不兼容了。

评论

51赞 Stefano Borini 7/16/2009
我认为这是一个有争议的问题。您正在对全局变量执行操作。在代码中任何涉及全局变量的计算现在都将(正确地)引用(“蓝莓”、“芒果”)。默认参数可以像任何其他情况一样。
71赞 Ben Blank 7/16/2009
实际上,我认为我不同意你的第一个例子。我不确定我是否喜欢一开始就这样修改初始值设定项的想法,但如果我这样做了,我希望它的行为与您描述的完全相同——将默认值更改为 .("blueberries", "mangos")
15赞 Lennart Regebro 7/16/2009
默认参数任何其他情况一样。出乎意料的是,该参数是全局变量,而不是局部变量。这反过来又是因为代码是在函数定义时执行的,而不是在调用时执行的。一旦你明白了这一点,课堂也是如此,这就非常清楚了。
28赞 alexis 10/9/2014
我发现这个例子具有误导性,而不是精彩。如果附加到而不是赋值,则 的行为将发生变化。当前精彩的设计就这么多。如果使用在其他地方引用的默认参数,然后从函数外部修改引用,则自找麻烦。真正的 WTF 是当人们定义一个新的默认参数(列表文字或对构造函数的调用)并且仍然得到位时。some_random_function()fruitseat()
29赞 user3467349 1/27/2015
您刚刚显式声明并重新分配了元组 - 如果之后工作方式不同,绝对不足为奇。globaleat
99赞 Lennart Regebro 7/16/2009 #2

原因很简单,绑定是在执行代码时完成的,并且执行了函数定义,嗯......定义函数时。

比较一下:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

这段代码也遭遇了完全相同的意外事件。Bananas 是一个类属性,因此,当你向它添加内容时,它会被添加到该类的所有实例中。原因完全一样。

它只是“它是如何工作的”,在函数情况下让它以不同的方式工作可能会很复杂,在类情况下可能是不可能的,或者至少会大大减慢对象实例化的速度,因为你必须保留类代码并在创建对象时执行它。

是的,这是出乎意料的。但是,一旦一分钱掉下来,它就完全符合 Python 的一般工作方式。事实上,这是一个很好的教具,一旦你理解了为什么会发生这种情况,你就会更好地理解 python。

也就是说,它应该在任何好的 Python 教程中占据突出地位。因为正如你提到的,每个人迟早都会遇到这个问题。

评论

0赞 Kieveli 7/16/2009
如何为类的每个实例定义不同的类属性?
21赞 Lennart Regebro 7/16/2009
如果每个实例都不同,则它不是类属性。类属性是 CLASS 上的属性。因此得名。因此,它们对于所有实例都是相同的。
2赞 Kieveli 7/16/2009
如何在类中定义一个属性,该属性对于类的每个实例都不同?(为那些无法确定不熟悉 Python 命名过程的人可能正在询问类的正常成员变量的人重新定义)。
3赞 Lennart Regebro 7/16/2009
@Kievieli:你说的是类的普通成员变量。您可以通过在任何方法中说 self.attribute = value 来定义实例属性。例如,__init__()。
0赞 Ethan Furman 1/7/2012
@Kieveli:两个答案:你不能,因为你在类级别定义的任何事物都将是一个类属性,任何访问该属性的实例都将访问相同的类属性;你可以,/sort of/,通过使用 s -- 这实际上是类级函数,它们的作用类似于普通属性,但将属性保存在实例中而不是类中(正如 Lennart 所说)。propertyself.attribute = value
58赞 ymv 7/16/2009 #3

这种行为很容易解释为:

  1. 函数(类等)声明只执行一次,创建所有默认值对象
  2. 一切都是通过引用传递的

所以:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a不会更改 - 每个赋值调用都会创建新的 int 对象 - 打印新对象
  2. b不会更改 - 新数组是从默认值构建并打印的
  3. c更改 - 对同一对象执行操作 - 并打印

评论

0赞 Anon 7/16/2009
(实际上,add 是一个不好的例子,但整数是不可变的仍然是我的主要观点。
0赞 Anon 7/16/2009
在检查后意识到这一点,当 b 设置为 [],b.__add__([1]) 返回 [1] 但即使列表是可变的,也会让 b 仍然 []。我的错。
0赞 Veky 5/8/2014
@ANon:有,但它不适用于 int。:-)__iadd__
42赞 Glenn Maynard 7/16/2009 #4

你要问的是为什么会这样:

def func(a=[], b = 2):
    pass

在内部不等同于此:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

除了显式调用 func(None, None) 的情况,我们将忽略它。

换句话说,为什么不存储每个参数,并在调用函数时评估它们,而不是评估默认参数呢?

一个答案可能就在那里——它会有效地将每个带有默认参数的函数变成闭包。即使这一切都隐藏在解释器中,而不是完全关闭,数据也必须存储在某个地方。它会更慢,并且会占用更多内存。

评论

10赞 Brian 7/16/2009
它不需要是一个闭包 - 更好的思考方式是简单地将创建默认值的字节码作为第一行代码 - 毕竟你正在编译正文 - 参数中的代码和正文中的代码之间没有真正的区别。
11赞 Lennart Regebro 7/16/2009
没错,但它仍然会减慢 Python 的速度,这实际上是相当令人惊讶的,除非你对类定义做同样的事情,这会使它变得非常慢,因为每次实例化一个类时,你都必须重新运行整个类定义。如前所述,修复将比问题更令人惊讶。
0赞 Jason Baker 7/16/2009
同意伦纳特的观点。正如 Guido 喜欢说的那样,对于每个语言功能或标准库,都有人在使用它。
9赞 Glenn Maynard 7/17/2009
現在改變它將是瘋狂的 - 我們只是在探索為什麼它會是這樣。如果它一开始就进行延迟违约评估,那也不一定令人惊讶。毫无疑问,这种核心的解析差异会对整个语言产生广泛的影响,并且可能有许多模糊的影响。
28赞 Jason Baker 7/16/2009 #5

这是一种性能优化。由于此功能,您认为这两个函数调用中哪一个更快?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

我会给你一个提示。这是拆卸(见 http://docs.python.org/library/dis.html):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

我怀疑有经验的行为是否有实际用途(谁真的在 C 中使用了静态变量,而不会滋生错误?

如您所见,使用不可变的默认参数时具有性能优势。如果它是一个经常被调用的函数,或者默认参数需要很长时间才能构造,这可能会有所不同。另外,请记住,Python 不是 C。在 C 语言中,你有几乎自由的常量。在 Python 中,你没有这个好处。

148赞 Utaal 7/16/2009 #6

我对 Python 解释器的内部工作原理一无所知(而且我也不是编译器和解释器方面的专家),所以如果我提出任何不明智或不可能的事情,请不要责怪我。

如果 python 对象是可变的,我认为在设计默认参数时应该考虑到这一点。 实例化列表时:

a = []

您希望获得由 引用的新列表。a

为什么要在a=[]

def x(a=[]):

在函数定义而不是调用时实例化一个新列表? 这就像你问“如果用户没有提供参数,那么实例化一个新列表,并像调用者一样使用它”。 我认为这是模棱两可的:

def x(a=datetime.datetime.now()):

用户,是否要默认为定义或执行时对应的日期时间? 在本例中,与上一个例一样,我将保持相同的行为,就好像默认参数“赋值”是函数的第一条指令(在函数调用时调用)。 另一方面,如果用户想要定义时间映射,他可以这样写:axdatetime.now()

b = datetime.datetime.now()
def x(a=b):

我知道,我知道:这是一个结束。或者,Python 可能会提供一个关键字来强制定义时绑定:

def x(static a=b):

评论

14赞 Anon 7/16/2009
你可以这样做: def x(a=None): 然后,如果 a 是 None,则设置 a=datetime.datetime.now()
35赞 AndreasT 4/22/2011
谢谢你。我真的无法解释为什么这让我无休止地烦恼。你已经做得很漂亮,只有最少的模糊和混乱。作为一个来自C++系统编程的人,有时天真地“翻译”语言功能,这个假朋友把我踢得很软,就像类属性一样。我理解为什么事情会这样,但我不禁不喜欢它,无论它会带来什么积极的影响。至少它与我的经历如此相反,我可能(希望)永远不会忘记它......
6赞 Karl Knechtel 7/23/2011
@Andreas一旦你使用 Python 足够长的时间,你就会开始看到 Python 将事物解释为类属性是多么合乎逻辑——只是因为 C++(以及 Java、C#等)等语言的特殊怪癖和局限性,块的内容被解释为属于实例才有意义:)但是,当类是第一类对象时,显然自然而然的事情是让它们的内容(在内存中)反映它们的内容(在代码中)。class {}
8赞 AndreasT 7/26/2011
规范结构在我的书中没有怪癖或限制。我知道它可能很笨拙和丑陋,但你可以称它为某事的“定义”。在我看来,动态语言有点像无政府主义者:当然每个人都是自由的,但你需要结构来让某人清空垃圾并铺平道路。猜猜我老了...... :)
8赞 Lutz Prechelt 3/30/2015
函数定义在模块加载时执行。函数在函数调用时执行。默认参数是函数定义的一部分,而不是函数体的一部分。(对于嵌套函数,它变得更加复杂。
71赞 Brian 7/16/2009 #7

我曾经认为在运行时创建对象会是更好的方法。我现在不太确定,因为您确实失去了一些有用的功能,尽管无论为了防止新手混淆,这可能都是值得的。这样做的缺点是:

1. 性能

def foo(arg=something_expensive_to_compute())):
    ...

如果使用调用时计算,则每次在没有参数的情况下使用函数时都会调用成本高昂的函数。你要么在每次调用时付出高昂的代价,要么需要在外部手动缓存值,从而污染命名空间并增加冗长。

2. 强制绑定参数

一个有用的技巧是在创建 lambda 时将 lambda 的参数绑定到变量的当前绑定。例如:

funcs = [ lambda i=i: i for i in range(10)]

这将返回返回 0,1,2,3...分别。如果行为发生更改,它们将绑定到 i 的调用时间值,因此您将获得所有返回的函数列表。i9

否则,实现此目的的唯一方法是使用 i 绑定创建进一步的闭包,即:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. 内省

考虑以下代码:

def foo(a='test', b=100, c=[]):
   print a,b,c

我们可以使用模块获取有关参数和默认值的信息,该模块inspect

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

这些信息对于文档生成、元编程、装饰器等非常有用。

现在,假设可以更改违约行为,以便这等效于:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

但是,我们已经失去了反省的能力,也无法查看默认参数是什么。由于对象尚未构造,因此如果不实际调用函数,我们就无法获取它们。我们能做的最好的事情就是存储源代码并将其作为字符串返回。

评论

2赞 yairchu 7/16/2009
如果每个函数都有一个函数来创建默认参数而不是值,您也可以实现内省。Inspect 模块将只调用该函数。
0赞 Brian 7/16/2009
@SilentGhost:我说的是行为是否被更改以重新创建它 - 创建一次是当前行为,以及为什么存在可变默认问题。
3赞 Brian 7/16/2009
@yairchu:这是假设结构是安全的(即没有副作用)。内省参数不应该做任何事情,但评估任意代码很可能最终会产生影响。
4赞 Glenn Maynard 7/17/2009
不同的语言设计通常意味着以不同的方式编写内容。你的第一个例子可以很容易地写成: _expensive = expensive();def foo(arg=_expensive),如果您特别不希望重新计算它。
0赞 Brian 7/17/2009
@Glenn - 这就是我所说的“在外部缓存变量” - 它有点冗长,但你最终会在命名空间中使用额外的变量。
29赞 Baczek 7/16/2009 #8

最简短的答案可能是“定义就是执行”,因此整个论点没有严格意义。作为一个更人为的例子,你可以引用这个:

def a(): return []

def b(x=a()):
    print x

希望这足以表明在语句执行时不执行默认参数表达式并不容易或没有意义,或两者兼而有之。def

不过,我同意当您尝试使用默认构造函数时,这是一个问题。

22赞 Christos Hayward 7/17/2009 #9

可能是真的:

  1. 有人正在使用每种语言/库功能,并且
  2. 在这里切换行为是不明智的,但是

坚持上述两个功能并仍然提出另一个观点是完全一致的:

  1. 这是一个令人困惑的功能,在 Python 中是不幸的。

其他答案,或者至少其中一些答案要么提出第 1 点和第 2 点,但没有提出第 3 点,或者提出第 3 点并淡化第 1 点和第 2 点。但这三个都是真的。

在这里,在中游切换马匹可能会造成严重的损坏,并且通过更改 Python 来直观地处理 Stefano 的开场片段可能会产生更多问题。而且,熟悉 Python 内部结构的人可能确实可以解释后果的雷区。然而

现有的行为不是 Pythonic,而 Python 之所以成功,是因为该语言几乎没有违反最小惊讶原则。这是一个真正的问题,无论将其连根拔起是否明智。这是一个设计缺陷。如果你通过尝试追踪行为来更好地理解语言,我可以说 C++ 可以完成所有这些甚至更多;例如,通过导航细微的指针错误,您可以学到很多东西。但这不是 Pythonic:那些足够关心 Python 的人,在面对这种行为时坚持不懈,是被这门语言所吸引的人,因为 Python 的惊喜比其他语言少得多。涉猎者和好奇者会成为 Pythonistas,因为他们惊讶于让某些东西工作所需的时间如此之短——不是因为设计失误——我的意思是,隐藏的逻辑谜题——这与程序员的直觉背道而驰,他们被 Python 所吸引,因为它只是工作

评论

7赞 Marcin 7/8/2012
-1 虽然这是一个站得住脚的观点,但这不是一个答案,我不同意它。太多的特殊例外会产生自己的极端情况。
6赞 Christos Hayward 12/28/2012
那么,说在 Python 中每次调用函数时保持 [] 的默认参数 [] 更有意义是“令人惊讶的无知”?
4赞 Christos Hayward 12/28/2012
将默认参数设置为 None 视为不幸的成语,然后在函数设置的主体中 if argument == None: argument = []?认为这个成语是不幸的,因为人们经常想要一个天真的新人所期望的,如果你分配 f(argument = []),argument 将自动默认为 [] 的值,这是否是无知的?
5赞 Christos Hayward 12/28/2012
但在 Python 中,语言精神的一部分是你不必进行太多的深入研究;array.sort() 有效,无论您对排序、big-O 和常量了解多少,都可以工作。Python 在数组排序机制中的美妙之处在于,您不需要深入研究内部结构。换句话说,Python 的美妙之处在于,人们通常不需要深入研究实现来获得 Just Works 的东西。并且有一个解决方法(...if argument == None: argument = []), 失败。
4赞 Jerry B 10/5/2013
作为一个独立的语句,该语句的意思是“创建一个空列表对象,并将名称'x'绑定到它。因此,在 中,还会创建一个空列表。它并不总是绑定到 x,因此它被绑定到默认代理项。稍后调用 f() 时,默认值将被拖出并绑定到 x。由于是空列表本身被抽走了,因此该列表是唯一可以绑定到 x 的东西,无论是否有任何东西卡在里面。不然怎么可能呢?x=[]def f(x=[])
1917赞 rob 7/18/2009 #10

实际上,这不是设计缺陷,也不是因为内部或性能。它只是因为 Python 中的函数是第一类对象,而不仅仅是一段代码。

一旦你这样想,那么它就完全有道理了:函数是一个对象,正在根据其定义进行评估;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改为另一个 - 与任何其他对象完全相同。

无论如何,effbot (Fredrik Lundh) 在 Python 中的默认参数值中对这种行为的原因进行了很好的解释。 我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。

评论

105赞 Cam Jackson 10/14/2011
对于任何阅读上述答案的人,我强烈建议您花时间通读链接的 Effbot 文章。除了所有其他有用的信息外,关于如何使用此语言功能进行结果缓存/记忆的部分非常方便!
160赞 gerrit 1/11/2013
即使它是第一类对象,人们可能仍然设想一种设计,其中每个默认值的代码与对象一起存储,并在每次调用函数时重新计算。我并不是说这样会更好,只是函数是第一类对象并不能完全排除它。
552赞 BlueRaja - Danny Pflughoeft 6/8/2013
对不起,任何被认为是“Python 中最大的 WTF”的东西都绝对是一个设计缺陷。在某些时候,这对每个人来说都是一个错误的来源,因为一开始没有人期望这种行为 - 这意味着它不应该一开始就这样设计。我不在乎他们必须跳过什么箍,他们应该设计 Python,以便默认参数是非静态的。
317赞 Mark Amery 1/9/2014
不管这是否是一个设计缺陷,你的回答似乎暗示着这种行为在某种程度上是必要的、自然的和显而易见的,因为函数是第一类对象,而事实并非如此。Python 有闭包。如果将默认参数替换为函数第一行的赋值,则它将计算每次调用的表达式(可能使用在封闭作用域中声明的名称)。完全没有理由说,每次以完全相同的方式调用函数时,都不可能或不合理地计算默认参数。
62赞 bukzor 5/4/2014
该设计并不直接遵循 。在您的范例中,建议将函数的默认值实现为属性而不是属性。functions are objects
34赞 Ben 5/23/2011 #11

这实际上与默认值无关,只是在编写具有可变默认值的函数时,它经常出现意外行为。

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

在此代码中看不到默认值,但您会遇到完全相同的问题。

问题在于,当调用方不希望这样做时,正在修改从调用方传入的可变变量。如果函数被调用为类似 ;然后,调用方将调用该函数以修改他们传入的值,并且该行为是预期的。但是这样的函数不太可能采用默认参数,并且可能不会返回列表(因为调用方已经引用了该列表;它刚刚传入的那个列表)。fooappend_5

带有默认参数的原始 ,不应修改它是显式传入还是获取默认值。您的代码应该保留可变参数,除非从上下文/名称/文档中可以清楚地看出应该修改参数。使用可变值作为参数作为本地临时参数传递是一个非常糟糕的主意,无论我们是否在 Python 中,也无论是否涉及默认参数。fooa

如果你需要在计算过程中破坏性地操纵一个局部临时,并且你需要从一个参数值开始你的操作,你需要创建一个副本。

评论

10赞 Andy Hayden 8/24/2012
虽然相关,但我认为这是截然不同的行为(因为我们希望“就地”改变)。默认的可变变量不会在每次调用时重新实例化是“意外”位......至少对我来说是这样。:)appenda
3赞 Mark Ransom 10/17/2017
@AndyHayden,如果该函数需要修改参数,为什么默认值是有意义的?
1赞 Mark Ransom 10/18/2017
@AndyHayden我在这里留下了我自己的答案,并扩展了这种情绪。让我知道你的想法。为了完整起见,我可能会将您的示例添加到其中。cache={}
2赞 Ben 10/18/2017
@AndyHayden 我的回答是,如果你对意外改变参数的默认值感到惊讶,那么你就会遇到另一个错误,那就是你的代码可能会在使用默认值时意外地改变调用者的值。请注意,如果 arg 是,则使用和分配真正的默认值并不能解决该问题(因此我认为这是一种反模式)。如果你通过避免改变参数值来修复另一个错误,无论它们是否有默认值,那么你永远不会注意到或关心这种“令人惊讶”的行为。NoneNone
1赞 Ben 10/18/2017
@AndyHayden 不过,这是一件微妙的事情,如果您描述的构造函数的调用者提供值而不是使用默认值,会发生什么?现在,您已经将对象的内部属性别名为调用方拥有的外部值!这种东西是难以追踪的错误的非常丰富的来源;这几乎比意外存储默认值然后对其进行更改更糟糕(因为如果您多次实例化该类,这至少应该很快显示出令人头疼的行为)。
17赞 Marcin 3/21/2012 #12

这里的解决方案是:

  1. 用作默认值(或随机数),并打开它以在运行时创建值;或Noneobject
  2. 使用 a 作为默认参数,并在 try 块中调用它以获取默认值(这就是 lambda 抽象的用途)。lambda

第二个选项很好,因为函数的用户可以传入一个可调用对象,该可调用对象可能已经存在(例如type)

评论

1赞 Flimm 3/30/2022
这并不能回答这个问题。
25赞 Dmitry Minkovsky 4/25/2012 #13

如果考虑到以下因素,则此行为不足为奇:

  1. 只读类属性在赋值尝试时的行为,以及
  2. 函数是对象(在公认的答案中解释得很好)。

(2)的作用已在此线程中广泛介绍。(1)可能是引起惊讶的因素,因为这种行为在来自其他语言时并不“直观”。

(1) 在 Python 类教程中进行了描述。尝试为只读类属性赋值时:

...在最内层范围之外找到的所有变量都是 只读(尝试写入此类变量只会创建一个 在最内层的作用域中新建局部变量,保留相同的 命名外部变量不变)。

回顾一下原来的例子,考虑以上几点:

def foo(a=[]):
    a.append(5)
    return a

下面是一个对象,是 (可在 上找到)的属性。由于是一个列表,是可变的,因此是 的读写属性。在实例化函数时,它被初始化为签名指定的空列表,只要函数对象存在,它就可用于读取和写入。fooafoofoo.func_defs[0]aafoo

在不覆盖缺省值的情况下进行调用时,将使用 该缺省值 from 。在本例中,用于函数对象的代码范围内。更改 ,它是对象的一部分,在执行 中的代码之间持续存在。foofoo.func_defsfoo.func_defs[0]aafoo.func_defs[0]foofoo

现在,将此与有关模拟其他语言的默认参数行为的文档中的示例进行比较,以便每次执行函数时都使用函数签名默认值:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

考虑到 (1)(2),我们可以看到为什么这会实现所需的行为:

  • 当函数对象被实例化时,设置为 ,一个不可变的对象。foofoo.func_defs[0]None
  • 当函数以默认值执行时(在函数调用中未指定任何参数),() 在本地作用域中可用。Lfoo.func_defs[0]NoneL
  • 在 上,赋值不能成功,因为该属性是只读的。L = []foo.func_defs[0]
  • 根据 (1),在局部作用域中创建一个名为 L 的新局部变量,并用于函数调用的其余部分。 因此,对于将来的调用,将保持不变。foo.func_defs[0]foo
315赞 glglgl 7/10/2012 #14

文档的相关部分:

执行函数定义时,默认参数值从左到右计算。这意味着在定义函数时,表达式将计算一次,并且每次调用都使用相同的“预先计算”值。当默认参数是可变对象(例如列表或字典)时,理解这一点尤其重要:如果函数修改了对象(例如,通过将项目附加到列表),则默认值实际上被修改了。这通常不是预期的。解决此问题的方法是用作默认值,并在函数主体中显式测试它,例如:None

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

评论

291赞 bukzor 5/4/2014
“这通常不是预期的”和“一种解决方法”这两个短语闻起来像是在记录设计缺陷。
16赞 Matthew 6/20/2014
@bukzor:陷阱需要被注意和记录,这就是为什么这个问题很好,并获得了如此多的赞成票。同时,陷阱不一定需要消除。有多少 Python 初学者将列表传递给修改它的函数,并震惊地看到更改出现在原始变量中?然而,当你了解如何使用可变对象类型时,它们就很棒了。我想这只是归结为对这个特定陷阱的看法。
49赞 holdenweb 12/19/2014
“这通常不是预期的”这句话的意思是“不是程序员真正想要发生的事情”,而不是“不是Python应该做的事情”。
20赞 code_dredd 10/3/2017
@holdenweb哇,我来晚了。考虑到上下文,bukzor 是完全正确的:他们记录了当他们决定语言应该执行函数定义时不是“预期”的行为/后果。由于这是他们设计选择的意外后果,因此这是一个设计缺陷。如果这不是一个设计缺陷,甚至没有必要提供“解决这个问题的方法”。
10赞 holdenweb 10/4/2017
我们可以把它拿去聊天,讨论它还能如何,但语义已经被彻底辩论了,没有人能想出一个合理的机制来创建-默认-值-随叫随到。一个严重的问题是,调用的范围通常与定义时的作用域完全不同,因此如果在调用时评估默认值,则无法确定名称解析。“绕过”意味着“你可以通过以下方式实现你想要的目的”,而不是“这是 Python 设计中的一个错误”。
43赞 hynekcer 11/23/2012 #15

1)所谓“可变默认参数”问题,一般来说是一个特殊的例子,说明:“所有有这个问题的函数在实际参数上也存在类似的副作用问题
,这违背了函数式编程的规则,
通常是不可回避的,应该一起修复。

例:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

解决方案:复制 一个绝对安全的解决方案是先复制或深度复制
输入对象,然后对副本执行任何操作。

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

许多内置的可变类型都有复制方法,如 或 或 可以很容易地复制,如 或 。每个对象也可以被复制或更彻底地被复制(如果可变对象由可变对象组成,后者很有用)。有些对象从根本上是基于“文件”对象等副作用的,不能通过复制有意义地复制。 复制some_dict.copy()some_set.copy()somelist[:]list(some_list)copy.copy(any_object)copy.deepcopy()

类似 SO 问题的示例问题

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

它不应保存在此函数返回的实例的任何公共属性中。(假设实例的私有属性不应该按照约定从这个类或子类的外部修改。 是私有属性)_var1

结论:
输入参数对象不应就地修改(突变),也不应将它们绑定到函数返回的对象中。(如果我们更喜欢没有副作用的编程,强烈建议这样做。 参见 Wiki 关于“副作用”(前两段在此上下文中是相关的。 .)

2)
仅当对实际参数的副作用是必需的,但对默认参数是不需要的,那么有用的解决方案是更多。
def ...(var1=None):if var1 is None:var1 = []

3)在某些情况下,默认参数的可变行为是否有用

评论

7赞 Veky 5/8/2014
我希望您知道 Python 是一种函数式编程语言。
11赞 hynekcer 5/8/2014
是的,Python 是一种具有一些功能特性的多参数语言。(“不要仅仅因为你有一把锤子就让每个问题看起来都像钉子。其中许多都在 Python 最佳实践中。Python 有一个有趣的 HOWTO 函数式编程 其他功能是闭包和咖喱,这里不做。
4赞 holdenweb 1/16/2018
我还要补充一点,在这个后期阶段,Python 的赋值语义已经明确设计为避免在必要时复制数据,因此创建副本(尤其是深度副本)将对运行时和内存使用产生不利影响。因此,它们应该只在必要时使用,但新来者往往难以理解何时使用。
1赞 hynekcer 1/18/2018
@holdenweb我同意。临时副本是保护原始可变数据免受可能修改它们的无关函数影响的最常见方法,有时也是唯一可能的方法。幸运的是,不合理地修改数据的函数被认为是一个错误,因此并不常见。
2赞 hynekcer 2/19/2018
@holdenweb 我的回答是关于其他答案中缺少什么有趣的东西。我希望每个读过它的人都会说:“哇,即使在更复杂的情况下,即使是健忘的痴迷程序员也存在解决方案,但我更喜欢......我正在下定决心,不要意外修改参数,“您在另一条评论中写道,”重新绑定该名称可以保证它永远不会被修改。(你的意思是重新绑定单个项目)在 Python 中,勤奋或复制是可以接受的自由代价,我喜欢它。
16赞 joedborg 1/15/2013 #16

你可以通过替换对象(以及与示波器的领带)来解决这个问题:

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

丑陋,但它有效。

评论

3赞 Michael Scott Asato Cuthbert 1/20/2013
如果您使用自动文档生成软件来记录函数预期的参数类型,这是一个很好的解决方案。如果 a 为 None,则将 a 设置为 [],如果 a 为 None,则将 a 设置为 [],这并不能帮助读者一目了然地理解预期的内容。
0赞 holdenweb 1/16/2018
很酷的想法:重新绑定该名称可以保证它永远不会被修改。我真的很喜欢。
0赞 Mark Ransom 5/26/2018
这正是做到这一点的方法。Python 不会复制参数,因此由您显式进行复制。一旦你有了副本,你就可以随心所欲地修改,而不会产生任何意想不到的副作用。
0赞 Flimm 3/30/2022
不过,这并不能回答这个问题。
22赞 hugo24 2/28/2013 #17

使用 None 的简单解决方法

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

评论

0赞 Flimm 3/30/2022
这不是问题的答案。
9赞 Norfeldt 7/22/2013 #18

这个“bug”给了我很多加班时间!但我开始看到它的潜在用途(但我仍然希望它在执行时出现)

我将给你们一个我认为有用的例子。

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

打印以下内容

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

评论

2赞 Mark Ransom 5/26/2021
你的例子似乎不是很现实。为什么要作为参数传递,而不是每次都从头开始?errors
5赞 user2384994 8/22/2013 #19

我认为这个问题的答案在于 python 如何将数据传递给参数(通过值或引用传递),而不是可变性或 python 如何处理“def”语句。

简要介绍。首先,python 中有两种类型的数据类型,一种是简单的基本数据类型,如数字,另一种数据类型是对象。其次,在将数据传递给参数时,python 按值传递基本数据类型,即将值的本地副本复制到局部变量,但通过引用传递对象,即指向对象的指针。

承认以上两点,我们来解释一下python代码发生了什么。这只是因为对象的引用传递,但与可变/不可变无关,或者可以说是“def”语句在定义时只执行一次。

[] 是一个对象,所以 python 将 [] 的引用传递给 ,即,只是一个指向 [] 的指针,它作为对象存在于内存中。但是,只有一个 [] 副本,其中包含许多引用。对于第一个 foo(),通过 append 方法将列表 [] 更改为 1。但请注意,列表对象只有一个副本,此对象现在变为 1。当运行第二个 foo() 时,effbot 网页上所说的(不再评估项目)是错误的。 被计算为列表对象,尽管现在该对象的内容为 1。这就是引用传递的效果!foo(3) 的结果可以很容易地用同样的方式推导出来。aaa

为了进一步验证我的答案,让我们看一下另外两个代码。

====== 2号 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]是一个对象,所以是(前者是可变的,而后者是不可变的。但可变性与问题无关)。没有在空间的某个地方,但我们知道它在那里,那里只有一个无的副本。因此,每次调用 foo 时,项目都会被评估为“无”(而不是“只计算一次”的答案),明确地说,是 None 的引用(或地址)。然后在 foo 中,item 更改为 [],即指向另一个具有不同地址的对象。None

====== 第 3 名 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

调用 foo(1) 会使 items 指向一个列表对象 [],地址为 11111111。在续集的 foo 函数中,列表的内容改为 1,但地址没有改动,还是11111111。然后 foo(2,[]) 来了。虽然 foo(2,[]) 中的 [] 在调用 foo(1) 时与默认参数 [] 的内容相同,但它们的地址是不同的!由于我们明确提供了参数,因此必须获取这个 new 的地址,比如 2222222,并在进行一些更改后返回它。现在 foo(3) 被执行了。由于提供了 only,因此 items 必须再次采用其默认值。默认值是多少?它是在定义 foo 函数时设置的:位于 11111111 中的列表对象。因此,这些项目被评估为具有元素 1 11111111地址。位于 2222222 处的列表还包含一个元素 2,但它不再由项目指向。因此,附加 3 将构成 [1,3]。items[]xitems

从上面的解释中,我们可以看出,被接受的答案中推荐的effbot网页未能给出这个问题的相关答案。更重要的是,我认为effbot网页中的一点是错误的。我认为有关UI的代码。按钮正确:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

每个按钮可以保存一个不同的回调函数,该函数将显示不同的值。我可以举一个例子来说明这一点:i

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

如果我们执行,我们将按预期得到 7,并将给出 9,另一个值为 。x[7]()x[9]()i

评论

6赞 Duncan 10/2/2013
你的最后一点是错误的。尝试一下,你会发现这是.x[7]()9
4赞 Veky 11/19/2014
“Python 按值传递基本数据类型,即将值本地复制到局部变量”是完全不正确的。令我惊讶的是,有人显然非常了解 Python,但对基础知识却有如此可怕的误解。:-(
16赞 Saish 9/12/2014 #20

当我们这样做时:

def foo(a=[]):
    ...

...如果调用方没有传递 A 的值,我们将参数分配给一个未命名的列表。a

为了简化此讨论,让我们暂时为未命名列表命名。怎么样 ?pavlo

def foo(a=pavlo):
   ...

在任何时候,如果调用方没有告诉我们是什么,我们就会重用 .apavlo

如果是可变的(可修改的),并最终修改它,则我们下次会注意到在不指定 的情况下调用效果。pavlofoofooa

所以这就是你所看到的(记住,初始化为 []):pavlo

 >>> foo()
 [5]

现在,是 [5]。pavlo

再次调用会再次修改:foo()pavlo

>>> foo()
[5, 5]

指定调用时间可确保不触及。afoo()pavlo

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

所以,仍然是.pavlo[5, 5]

>>> foo()
[5, 5, 5]
16赞 bgreen-litl 2/6/2015 #21

我有时会利用此行为作为以下模式的替代方法:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

如果仅由 ,我喜欢以下模式作为替代:singletonuse_singleton

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

我用它来实例化访问外部资源的客户端类,也用它来创建字典或列表以进行记忆。

由于我不认为这种模式是众所周知的,所以我做了一个简短的评论,以防止将来的误解。

评论

2赞 Stefano Borini 2/6/2015
我更喜欢添加一个用于记忆的装饰器,并将记忆缓存放在函数对象本身上。
0赞 Yann Vernier 11/19/2017
此示例不会替换您显示的更复杂的模式,因为在默认参数示例中,您在 def 时间调用,但在全局示例中,则在调用时调用。真正的替换将使用某种可变框作为默认参数值,但添加参数会使传递替代值的机会。_make_singleton
29赞 Stéphane 3/27/2015 #22

已经很忙的话题,但从我在这里读到的内容来看,以下内容帮助我意识到它在内部是如何工作的:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

评论

4赞 Jörn Hees 4/8/2017
实际上,对于新人来说,这可能有点令人困惑,因为超载......考虑将其更改为并添加一行。这将使两个列表上总是创建一个新列表(分配给 ),而修改后的列表仍然可以具有相同的 .a = a + [1]ab = a + [1] ; print id(b)a.append(2)+baid(a)
65赞 Lutz Prechelt 3/30/2015 #23

Python 的 5 点防御

  1. 简单性:从以下意义上讲,行为很简单: 大多数人只落入这个陷阱一次,而不是几次。

  2. 一致性:Python 始终传递对象,而不是名称。 显然,默认参数是函数的一部分 标题(不是函数正文)。因此,应该对其进行评估 在模块加载时(并且仅在模块加载时,除非嵌套),而不是 在函数调用时。

  3. 有用性:正如弗雷德里克·伦德(Frederik Lundh)在他的解释中指出的那样 在“Python 中的默认参数值”中, 当前行为对于高级编程非常有用。 (谨慎使用。

  4. 足够的文档:在最基本的 Python 文档中, 教程中,问题被大声宣布为 “有关定义函数的更多信息”一节第一小节中的“重要警告”。 警告甚至使用粗体, 这很少应用于标题之外。 RTFM:阅读精美的手册。

  5. 元学习:掉进陷阱其实是很 有帮助的时刻(至少如果你是一个反思性的学习者), 因为你随后会更好地理解这一点 上面的“一致性”,这将 教你很多关于 Python 的知识。

评论

24赞 oriadam 9/5/2015
我花了一年时间才发现这种行为在生产中搞砸了我的代码,最终删除了一个完整的功能,直到我偶然遇到这个设计缺陷。我正在使用 Django。由于暂存环境没有很多请求,因此此错误从未对 QA 产生任何影响。当我们上线并收到许多同时请求时 - 一些实用程序函数开始覆盖彼此的参数!制造安全漏洞、错误等等。
22赞 Wildcard 8/30/2016
@oriadam,没有冒犯,但我想知道你是如何在没有遇到这种情况的情况下学习 Python 的。我现在只是在学习 Python,这个可能的陷阱在官方 Python 教程中提到,同时第一次提到默认参数。(如本答案第 4 点所述。我想寓意是——相当冷漠地——阅读你用来创建生产软件的语言的官方文档
2赞 Vatine 9/2/2016
此外,如果除了我正在进行的函数调用之外还调用了一个复杂度未知的函数,那将是令人惊讶的(对我来说)。
8赞 Robin De Schepper 1/5/2021
@oriadam,您的公司需要在拥有开发、暂存和生产环境时,使用他们编写的语言进行代码审查和实际的专家编码人员。新手错误和不良的代码习惯不应该进入生产代码
2赞 Matthew Read 12/19/2022
高级程序员无法神奇地捕获所有错误。代码审查是极好且必要的,但不要假装它们可以阻止“新手错误”进入生产环境。甚至专家也会偶尔自己引入此类错误。
8赞 rassa45 5/26/2015 #24

只需将函数更改为:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

评论

0赞 Flimm 3/30/2022
不过,这并不能回答这个问题。
20赞 Alexander 9/12/2015 #25

我将演示一种将默认列表值传递给函数的替代结构(它同样适用于字典)。

正如其他人所广泛评论的那样,list 参数在定义函数时绑定到函数,而不是在执行函数时绑定。由于列表和字典是可变的,因此对此参数的任何更改都将影响对此函数的其他调用。因此,对函数的后续调用将收到此共享列表,该列表可能已被对该函数的任何其他调用更改。更糟糕的是,两个参数同时使用此函数的共享参数,而忽略了另一个参数所做的更改。

错误的方法(可能...)

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

您可以使用以下命令验证它们是否为同一对象:id

>>> id(a)
5347866528

>>> id(b)
5347866528

根据 Brett Slatkin 的“Effective Python: 59 Specific Ways to Write Better Python”,第 20 项:使用 None 和 Docstrings 指定动态默认参数(第 48 页)

在 Python 中实现预期结果的约定是 提供默认值 和 以记录实际行为 在文档字符串中。None

此实现可确保对函数的每次调用都接收默认列表或传递给函数的列表。

首选方法

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

“错误方法”可能存在合法的用例,即程序员打算共享默认列表参数,但这更有可能是例外而不是规则。

89赞 Dimitris Fasarakis Hilliard 12/9/2015 #26

你为什么不反省?

真的很惊讶没有人对可调用对象执行 Python(和 apply)提供的有见地的内省。23

给定一个简单的小函数,定义为:func

>>> def func(a = []):
...    a.append(5)

当 Python 遇到它时,它要做的第一件事就是编译它,以便为此函数创建一个对象。完成此编译步骤后,Python 会评估*,然后将默认参数(此处为空列表 []存储在函数对象本身中。正如上面的答案所提到的:现在可以将列表视为函数的成员codeafunc

因此,让我们做一些内省,在函数对象内部检查列表是如何扩展的。我为此使用,对于 Python 2,这同样适用(在 Python 2 中使用 or;是的,同一事物的两个名称)。Python 3.x__defaults__func_defaults

执行前的功能:

>>> def func(a = []):
...     a.append(5)
...     

Python 执行此定义后,它将获取指定的任何默认参数(此处),并将它们塞入函数对象的 __defaults__ 属性中(相关部分:Callables):a = []

>>> func.__defaults__
([],)

好的,所以一个空列表作为 中的单个条目,正如预期的那样。__defaults__

执行后的功能:

现在让我们执行这个函数:

>>> func()

现在,让我们再看看这些:__defaults__

>>> func.__defaults__
([5],)

惊讶?对象内部的值会发生变化!现在,对函数的连续调用将简单地追加到该嵌入对象:list

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

所以,你有它,发生这个“缺陷”的原因,是因为默认参数是函数对象的一部分。这里没有什么奇怪的事情发生,只是有点令人惊讶。

解决此问题的常见解决方案是用作默认值,然后在函数体中初始化:None

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

由于函数体每次都会重新执行,因此如果没有为 传递参数,则始终会得到一个新的空列表。a


要进一步验证中的列表是否与函数中使用的列表相同,只需更改函数以返回函数体中使用的列表。然后,将它与 (position in ) 中的列表进行比较,您将看到它们确实如何引用同一个列表实例:__defaults__funcida__defaults__[0]__defaults__

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

一切都带有内省的力量!


*要验证 Python 是否在函数编译期间评估默认参数,请尝试执行以下命令:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

您会注意到,在生成函数并将其绑定到名称的过程之前被调用。input()bar

评论

1赞 das-g 3/9/2016
是否需要进行最后一次验证,或者操作员会回答相同的问题?id(...)is
2赞 Dimitris Fasarakis Hilliard 3/9/2016
@das-g 就可以了,我只是用了,因为我认为它可能更直观。isid(val)
0赞 Brilliand 10/18/2019
使用默认值会严重限制内省的有用性,因此我认为这并不能很好地为工作方式辩护。延迟评估将做更多的事情来保持函数默认值对双方都有用。None__defaults____defaults__
33赞 Russia Must Remove Putin 5/2/2016 #27

Python:可变的默认参数

默认参数在函数编译为函数对象时,在程序运行时开始时进行计算。当被函数多次使用时,它们在内存中保持相同的对象,并且当发生突变时(如果对象是可变类型),它们在连续调用时保持突变。

它们会发生突变并保持突变状态,因为每次调用函数时它们都是同一个对象。

等效代码:

由于在编译和实例化函数对象时,列表绑定到函数,因此:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

几乎完全等同于此:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

示范

下面是一个演示 - 每次引用它们时,您可以验证它们是否是同一个对象

  • 看到列表是在函数完成编译到函数对象之前创建的,
  • 观察到每次引用列表时 ID 都相同,
  • 观察到当第二次调用使用它的函数时,列表保持更改,
  • 观察从源打印输出的顺序(我方便地为您编号):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

并使用以下命令运行它:python example.py

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

这是否违反了“最小惊讶”原则?

这种执行顺序经常让 Python 的新用户感到困惑。如果您了解 Python 执行模型,那么它就会变得非常意料之中。

给 Python 新用户的常规说明:

但这就是为什么对新用户的通常指示是创建他们的默认参数,如下所示:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

这使用 None 单例作为哨兵对象来告诉函数我们是否获得了默认值以外的参数。如果我们没有得到任何参数,那么我们实际上想使用一个新的空列表,作为默认值。[]

正如有关控制流的教程部分所说:

如果您不希望在后续调用之间共享默认值, 您可以像这样编写函数:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
8赞 Mark Ransom 10/18/2017 #28

这不是设计缺陷。任何被此绊倒的人都做错了什么。

我看到有 3 种情况您可能会遇到此问题:

  1. 您打算修改参数作为函数的副作用。在这种情况下,使用默认参数是没有意义的。唯一的例外是当你滥用参数列表来具有函数属性时,例如 ,你根本不会用实际的参数来调用函数。cache={}
  2. 您打算不修改参数,但您不小心修改了它。这是一个错误,修复它。
  3. 您打算修改参数以在函数内部使用,但不希望在函数外部查看修改。在这种情况下,您需要复制参数,无论它是否是默认的!Python 不是一种按值调用的语言,所以它不会为你制作副本,你需要明确它。

问题中的例子可能属于第 1 类或第 3 类。奇怪的是,它既修改了传递的列表,又返回了它;你应该选择一个或另一个。

评论

1赞 Andy Hayden 10/18/2017
“做错事”是诊断。也就是说,我认为有时 =None 模式是有用的,但通常在这种情况下传递可变 (2) 时您不想修改。该模式实际上是一个仅限面试的解决方案,在实际代码中,您可能希望@lru_cachecache={}
5赞 aCuria 5/25/2021
完全不同意,在许多情况下,这绝对是一个设计缺陷,而不是程序员在做某事
1赞 qwr 5/26/2021
我从来没有遇到过 OP 的问题,即使它得到了如此高的赞成,因为让默认参数可变对我来说是奇怪的设计。
2赞 Clement Cherlin 6/3/2021
@MarkRansom 如果我们认为副作用是可以的,那么修改默认参数作为副作用函数的一部分并没有错。假设您有一个函数,该函数对列表执行某些操作并返回该列表。我们希望确保该函数始终返回一个列表。然后,将空(或非空)列表作为默认值是完全有意义的。该语言违反了很大一部分新 Python 程序员的期望。为什么他们错了,而语言是正确的?如果语言具有相反的行为,你会提出相反的论点吗?
2赞 Clement Cherlin 6/7/2021
@MarkRansom 不,他们不是;例如,JavaScript 就没有这个设计缺陷
8赞 MisterMiyagi 12/15/2018 #29

TLDR:定义时间默认值是一致的,并且严格来说更具表现力。


定义函数会影响两个作用域:包含函数的定义作用域和函数包含的执行作用域。虽然很清楚块如何映射到范围,但问题是属于哪里:def <name>(<args=defaults>):

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

该部件必须在定义范围内进行评估 - 毕竟,我们希望在那里可用。仅评估其内部的功能将使其无法访问。def namename

由于是一个常量名称,因此我们可以与 同时“计算”它。这也有一个优点,它生成具有已知签名的函数,而不是裸 。parameterdef namename(parameter=...):name(...):

现在,什么时候评估?default

一致性已经说“在定义上”:其他所有内容在定义时也最好进行评估。推迟其中的部分内容将是令人惊讶的选择。def <name>(<args=defaults>):

这两个选项也不等效:如果在定义时进行计算,它仍然会影响执行时间。如果在执行时计算,则不会影响定义时间。选择“at definition”允许表达两种情况,而选择“at execution”只能表达一种情况:defaultdefault

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

评论

2赞 LarsH 9/23/2019
“一致性已经说'在定义上':其他一切都最好在定义上进行评估。我不认为结论是从前提得出的。仅仅因为两件事在同一条线上并不意味着它们应该在同一范围内进行评估。 与该行的其余部分不同:它是一种表达方式。计算表达式与定义函数是一个非常不同的过程。def <name>(<args=defaults>):default
1赞 MisterMiyagi 9/23/2019
@LarsH 函数定义都是在 Python 中计算的。无论它来自语句 () 还是表达式 () 都不会改变创建函数意味着评估——尤其是对其签名的评估。默认值是函数签名的一部分。这并不意味着必须立即评估默认值 - 例如,类型提示可能不需要。但它肯定表明他们应该这样做,除非有充分的理由不这样做。deflambda
2赞 LarsH 9/24/2019
好吧,创建函数意味着在某种意义上进行计算,但显然不是在定义时计算其中的每个表达式的意义。大多数都不是。我不清楚在定义时,签名在什么意义上被特别“评估”,就像函数体被“评估”(解析为合适的表示)一样;而函数体中的表达式显然不是完全意义上的。从这个角度来看,一致性意味着签名中的表达式也不应该被“完全”评估。
2赞 LarsH 9/24/2019
我并不是说你错了,只是你的结论不能仅仅来自一致性。
0赞 MisterMiyagi 9/24/2019
@LarsH 违约既不是正文的一部分,我也不是说一致性是唯一的标准。你能提出一个建议,如何澄清答案吗?
15赞 Przemek D 1/3/2019 #30

所有其他答案都解释了为什么这实际上是一种很好且理想的行为,或者为什么您无论如何都不需要它。我的是给那些固执的人,他们想行使自己的权利,使语言屈服于他们的意志,而不是相反。

我们将使用装饰器“修复”此行为,该装饰器将复制默认值,而不是为每个位置参数重复使用相同的实例,保留其默认值。

import inspect
from copy import deepcopy  # copy would fail on deep arguments like nested dicts

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(deepcopy(arg))
        return function(*new_args, **kw)
    return wrapper

现在让我们使用这个装饰器重新定义我们的函数:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

这对于采用多个参数的函数尤其简洁。比较:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

需要注意的是,如果您尝试使用关键字参数,上述解决方案会中断,如下所示:

foo(a=[4])

可以调整装饰器以允许这一点,但我们将其作为读者的练习;)

评论

1赞 Flimm 3/30/2022
如果默认参数很深,这也会中断,例如 .只有顶级词典按值复制,其他词典按引用复制。出现此问题的原因是您使用了而不是{"grandparent": {"parent": {"child": "value"}}}copydeepcopy
1赞 Przemek D 3/31/2022
@Flimm我发现你的短语“这打破了”相当不公平,因为它似乎表明整个概念在某种程度上存在缺陷,而实际上它只是实现的一个小细节。但感谢您的评论,我将编辑和改进我的答案。
-3赞 Charles Merriam 1/8/2020 #31

有一种简单的方法可以理解为什么会发生这种情况。

Python 在命名空间中从上到下执行代码。

“内部”只是这一规则的体现。

做出这种选择的原因是“让语言适合你的头脑”。所有奇怪的极端情况都倾向于简化为在命名空间中执行代码:默认不可变、嵌套函数、类(编译完成后有一点修补)、self 参数等。同样,复杂的语法可以用简单的语法来编写:is just .这适用于列表推导式;装饰;元类;和更多。这使您可以近乎完美地看到奇怪的角落。语言适合你的头脑。a.foo(...)a.lookup('foo').__call__(a,...)

你应该坚持下去。学习 Python 有一段时间对这门语言的不满,但它会变得很舒服。这是我工作过的唯一一种语言,你越看极端情况就越简单。

继续黑客攻击!做笔记。

对于您的特定代码,过于详细:

def foo(a=[]):
    a.append(5)
    return a

foo()

是一个语句,等同于:

  1. 开始创建代码对象。
  2. 现在解释,正如我们所走。是参数 a 的默认值。它一如既往地属于列表类型。(a=[])[][]
  3. 将 之后的所有代码编译成 Python 字节码,并将其粘贴到另一个列表中。:
  4. 创建可调用字典,参数和代码位于“code”字段中
  5. 将可调用对象添加到当前命名空间的“foo”字段中。

然后,它转到下一行 .foo()

  1. 它不是保留字,因此请在命名空间中查找它
  2. 调用该函数,该函数将使用列表作为默认参数。开始在其命名空间中执行其字节码。
  3. append不创建新列表,因此修改了旧列表。
12赞 Flimm 3/30/2022 #32

是的,这是 Python 中的设计缺陷

我已经阅读了所有其他答案,但我不相信。这种设计确实违反了最小惊讶原则。

默认值可以设计为在调用函数时计算,而不是在定义函数时计算。Javascript 是这样做到的:

function foo(a=[]) {
  a.push(5);
  return a;
}
console.log(foo()); // [5]
console.log(foo()); // [5]
console.log(foo()); // [5]

为了进一步证明这是一个设计缺陷,Python 核心开发人员目前正在讨论引入新的语法来解决这个问题。请参阅本文:Python 的后期绑定参数默认值

为了证明这是一个设计缺陷的更多证据,如果你在谷歌上搜索“Python gotchas”,这个设计在前 9 个 Google 结果(123456789)中被提及为一个陷阱,通常是列表中的第一个陷阱。相比之下,如果你在谷歌上搜索“Javascript gotchas”,Javascript 中默认参数的行为甚至一次都不会被提及为陷阱。

顾名思义,Gotchas 违反了最小惊讶原则。他们感到惊讶。鉴于默认参数值的行为存在高级设计,不可避免的结论是 Python 在这里的行为代表了设计缺陷。

我是作为一个热爱 Python 的人这么说的。我们可以成为 Python 的粉丝,并且仍然承认,每个对 Python 的这一方面感到不愉快的人都会感到不愉快的惊讶,因为它是一个真正的“陷阱”。

评论

1赞 Lutz Prechelt 4/26/2023
称其为设计缺陷的假设是有更好的解决方案。但是这个解决方案(无论选择哪一个)都会打破Python的正交性(有罪的行为从中产生),这将在其他地方造成更多的惊喜。
1赞 Flimm 4/26/2023
@LutzPrechelt 正如我在回答中提到的,有一个更好的设计,即用于其他一些编程语言的设计,比如 JavaScript。我不明白更好的设计会如何破坏 Python 中的东西,除了向后兼容性。“正交性”是什么意思?
1赞 Lutz Prechelt 4/27/2023
通过正交性,我的意思是在 Python 中是在模块加载时执行的语句(对于顶级函数)。默认参数定义是该语句的一部分,因此也应该在加载时执行 - 任何其他行为也会令人惊讶。def
2赞 Flimm 4/27/2023
@LutzPrechelt 仅仅因为语句是在模块加载时执行的,这并不意味着默认的 arg 值也应该在模块加载时执行。以声明为例。如果在模块加载时打印出来,你会感到惊讶吗?我知道我会的,即使此语句是在模块加载时执行的。直观地说,我们期望此语句的部分仅在调用 lambda 后执行。同样,正如人们有据可查的惊讶所证明的那样,我们希望在调用时执行默认值deffoobar = lambda x: print("hi")hiprint
1赞 Lutz Prechelt 4/29/2023
en.wikipedia.org/wiki/Orthogonality_(编程)是对这个想法的一个很好的讨论。
1赞 david 2/10/2023 #33

文档失败

def fn(<mutable object> = [manifest constant]):

此 python 语法用于可变对象的可选初始值设定项。将清单常量称为默认值既错误又令人困惑。特别不幸的是,官方的 python 文档使用了误导性的描述,并且这个令人痛苦的误导性术语已被复制到网络上,包括在这里的问题中。

通过更正文档并不能完全解决问题:人们仍然希望 python 为可变对象实现默认值。但这将是一个开始。

1赞 Karl Knechtel 3/27/2023 #34

来自命令查询分离的参数

Python 非常尊重命令查询分离的原则。例如,普通赋值被视为语句,而不是表达式,因此不能用作子表达式;许多内置和标准库方法主要通过修改对象来工作时,会返回 None,而不是对象。

我们可以提出一个简单的论点,反对对参数使用可变的默认值,如下所示:

  1. 有两种方法可以从函数中获取信息:通过操作它,或者通过修改封闭作用域中的某些变量。return

  2. 但是,“修改封闭范围内的变量”的唯一合理的、可重用的方法是修改其中一个参数。修改函数本身会使界面更加尴尬(并可能导致递归代码出现问题)。修改其他任何内容都需要函数和调用方共享该上下文;这意味着会污染全局命名空间,并再次创建一个笨拙且高度非标准的接口。

  3. 默认参数值的要点是能够在没有相应参数的情况下调用函数;因此,我们必须针对发生这种情况的情况进行设计。

  4. 假设我们在没有相应参数的情况下调用该函数,因此使用默认值。我们将如何发出变化的信号?默认值的对象不在调用方的作用域中,并且它不是函数的属性;因此,调用方不容易访问它。因此,我们无法通过现在变异的默认值轻松获取信息。

  5. 因此,为了传达结果,我们必须提供一些信息。return

  6. 为了使接口保持一致,并避免使用接口的复杂特殊情况,因此该函数应一致地返回计算结果。

  7. 但是,由于命令查询分离,这意味着该函数不应修改参数。我们不能两者兼而有之,因为那样的话,“提出问题可能会改变答案”。

  8. 由于该函数不会修改参数,因此它不会修改默认值对象。

  9. 但是,如果对象不会被修改,则没有理由让它可变。使用可变类型会误导性地暗示要改变值。

在实践中,人们有时会编写默认参数,即使代码不会改变它们。用 替换它很容易,但表示“一个空的、不可变的映射”并非易事。当然,使用(不可变的)作为哨兵,然后显式检查该值,巧妙地回避了这个问题。{}[]()None