任何人只要长时间摆弄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中使用了静态变量,而没有滋生bug?)

编辑:

Baczek举了一个有趣的例子。连同您的大多数评论,特别是Utaal的评论,我进一步阐述了:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

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

在函数内部进行绑定意味着当函数被调用而不是被定义时,x被有效地绑定到指定的默认值,这将带来一个严重的缺陷:def行将是“混合”的,即部分绑定(函数对象)将在定义时发生,部分绑定(默认参数的赋值)将在函数调用时发生。

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


当前回答

这可能是真的:

有人正在使用每种语言/库功能,并且在这里改变行为是不明智的,但是

坚持上述两个特征是完全一致的,并且仍然提出另一点:

这是一个令人困惑的特性,在Python中很不幸。

其他答案,或至少其中一些答案,要么是第1点和第2点,但不是第3点,要么就是第3点而淡化第1点或第2点。但这三个都是真的。

在这里,在中途换马可能会导致严重的破坏,而且通过改变Python来直观地处理Stefano的开头片段可能会产生更多的问题。也许有人很了解Python的内部结构,就能解释一个后果雷区。然而

现有的行为不是Pythonic的,Python之所以成功,是因为该语言几乎没有违反最不令人惊讶的原则。这是一个真正的问题,无论根除它是否明智。这是一种设计缺陷。如果你通过尝试追踪行为来更好地理解语言,我可以说C++完成了所有这些以及更多的工作;例如,通过导航细微的指针错误,您可以学到很多东西。但这并不是Pythonic的:那些对Python足够关心并在这种行为面前坚持不懈的人都是被这种语言所吸引的人,因为Python比其他语言的惊喜要少得多。当他们惊讶于用很少的时间就能让一些东西发挥作用——而不是因为设计失误——我的意思是,隐藏的逻辑谜题——这违背了程序员的直觉时,达博人和好奇者就成了Python爱好者,因为Python很好用。

其他回答

Python防御5分

简单:行为在以下意义上很简单:大多数人只会陷入一次,而不是几次。一致性:Python始终传递对象,而不是名称。显然,默认参数是函数的一部分标题(而不是函数体)。因此,应该对其进行评估在模块加载时(并且仅在模块加载时间,除非嵌套),而不是在函数调用时。有用性:正如Frederik Lundh在解释中指出的在“Python中的默认参数值”中当前行为对于高级编程非常有用。(谨慎使用。)足够的文档:在最基本的Python文档中,在教程中,这个问题被大声宣布为第节第一小节中的“重要警告”“更多关于定义函数”。警告甚至使用粗体,这很少应用于标题之外。RTF:阅读详细手册。元学习:落入陷阱实际上是一个非常有帮助的时刻(至少如果你是一个反思型学习者),因为你随后会更好地理解这一点上述“一致性”将教你很多关于Python的知识。

文件的相关部分:

执行函数定义时,从左到右计算默认参数值。这意味着在定义函数时,表达式将求值一次,并且每次调用都使用相同的“预计算”值。当默认参数是可变对象(例如列表或字典)时,这一点尤其重要:如果函数修改了对象(例如,通过将项附加到列表),则默认值实际上已被修改。这通常不是预期的。解决此问题的一种方法是使用None作为默认值,并在函数体中显式测试它,例如:def whats_on_the_telly(企鹅=无):如果企鹅为无:企鹅=[]企鹅追加(“动物园的财产”)返回企鹅

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

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

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

当我们这样做时:

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

…如果调用者没有传递a的值,我们将参数a分配给未命名列表。

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

def foo(a=pavlo):
   ...

在任何时候,如果调用者没有告诉我们a是什么,我们就重用pavlo。

如果pavlo是可变的(可修改的),而foo最终对其进行了修改,那么在下次调用foo时我们会注意到这样的效果,而不指定a。

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

 >>> foo()
 [5]

现在,帕夫洛是[5]。

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

>>> foo()
[5, 5]

在调用foo()时指定a可确保不会触及pavlo。

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

所以,帕夫洛仍然是[5]。

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

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=无):如果var1为无:var1=[]更多。。

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