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

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


当前回答

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

其他回答

只需将函数更改为:

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

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

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

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

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

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绑定到i的调用时间值,因此您将得到一个函数列表,所有函数都返回了9。

否则,实现这一点的唯一方法是使用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=[]

然而,我们已经失去了自省的能力,无法看到默认参数是什么。因为对象还没有被构造,所以我们无法在不调用函数的情况下获取它们。我们所能做的最好的方法是存储源代码并将其作为字符串返回。

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

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

错误的方法(可能…):

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]

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

我有时会利用这种行为来替代以下模式:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

如果singleton仅由use_singleton使用,我喜欢以下模式作为替换:

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

我用它来实例化访问外部资源的客户机类,也用来创建用于内存化的字典或列表。

由于我不认为这种模式是众所周知的,所以我确实发表了简短的评论,以防止未来的误解。