任何人只要长时间摆弄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(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的《有效的Python:59种编写更好Python的具体方法》,第20项:使用None和Docstring指定动态默认参数(第48页)

在Python中实现所需结果的惯例是提供默认值None,并记录实际行为在docstring中。

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

首选方法:

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]

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

其他回答

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) 在某些情况下,默认参数的可变行为很有用。

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都是相同的,观察到当第二次调用使用该列表的函数时该列表保持改变,观察从源打印输出的顺序(我方便地为您编号):

示例.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单例作为一个sentinel对象来告诉函数我们是否得到了默认值以外的参数。如果没有参数,那么我们实际上希望使用新的空列表[]作为默认值。

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

如果您不希望在后续调用之间共享默认值,您可以改为这样编写函数:定义f(a,L=无):如果L为无:L=[]L.附加(a)返回L

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

def a(): return []

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

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

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

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

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

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

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

Python防御5分

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