任何人只要长时间摆弄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行将是“混合”的,即部分绑定(函数对象)将在定义时发生,部分绑定(默认参数的赋值)将在函数调用时发生。

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


当前回答

这实际上与默认值无关,只是当您使用可变默认值编写函数时,它通常会出现意外行为。

>>> 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]

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

问题是,foo正在修改从调用方传入的可变变量,而调用方并不期望这样做。如果函数的调用类似于append_5,那么这样的代码就可以了;那么调用者将调用函数以修改传入的值,并且行为是预期的。但是这样的函数不太可能采用默认参数,并且可能不会返回列表(因为调用者已经有了对该列表的引用;它刚刚传入的那个)。

您的原始foo(带有默认参数)不应该修改a,无论它是显式传入还是获得默认值。除非从上下文/名称/文档中可以清楚地看到参数应该被修改,否则代码应该保留可变参数。无论我们是否使用Python,也不管是否涉及默认参数,使用作为参数传入的可变值作为本地临时变量是一个非常糟糕的想法。

如果在计算过程中需要破坏性地操作本地临时变量,并且需要从参数值开始操作,则需要创建副本。

其他回答

Python防御5分

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

这个“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

这种行为很容易解释为:

函数(类等)声明只执行一次,创建所有默认值对象所有内容都通过引用传递

So:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c

a不改变-每次赋值调用都创建新的int对象-打印新对象b不变-新数组是从默认值构建并打印的c更改-对同一对象执行操作-并打印

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

比较一下:

class BananaBunch:
    bananas = []

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

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

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

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

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

每个其他的答案都解释了为什么这实际上是一个好的和期望的行为,或者为什么你无论如何都不需要这个。我是为那些顽固的人准备的,他们想行使自己的权利,让语言服从自己的意愿,而不是相反。

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

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

with

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

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

foo(a=[4])

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