任何人只要长时间摆弄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:可变默认参数
将函数编译为函数对象时,将计算默认参数。当被该函数多次使用时,它们仍然是同一个对象。
当它们是可变的时,当它们发生突变时(例如,通过向其中添加元素),它们在连续调用时保持突变。
它们保持变异,因为它们每次都是同一个物体。
等效代码:
由于在编译和实例化函数对象时列表绑定到函数,因此:
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
是的,这是Python中的一个设计缺陷
我看过所有其他答案,但我不相信。这种设计确实违反了最小惊讶的原则。
默认值可以设计为在调用函数时计算,而不是在定义函数时计算。Javascript是这样做的:
函数foo(a=[]){a.推动(5);返回a;}console.log(foo());//[5]console.log(foo());//[5]console.log(foo());//[5]
作为进一步证明这是一个设计缺陷的证据,Python核心开发人员目前正在讨论引入新语法来解决这个问题。请参阅本文:Python的后期绑定参数默认值。
为了进一步证明这是一个设计缺陷,如果你搜索“Python gotchas”,这个设计被称为gotcha,通常是列表中的第一个gotcha,在前9个Google结果(1、2、3、4、5、6、7、8、9)中。相反,如果你搜索“Javascript gotchas”,Javascript中默认参数的行为甚至一次都没有被提到过。
根据定义,Gotchas违反了最小惊讶的原则。它们令人惊讶。鉴于默认参数值的行为有着更高级的设计,不可避免的结论是Python的行为在这里代表了一个设计缺陷。
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解释器的内部工作一无所知(我也不是编译器和解释器的专家),所以如果我提出任何不合理或不可能的建议,不要怪我。
假设python对象是可变的,我认为在设计默认参数时应该考虑到这一点。实例化列表时:
a = []
你希望得到一个新的列表。
为什么a=[]
def x(a=[]):
在函数定义而不是调用上实例化新列表?这就像你在问“如果用户不提供参数,那么实例化一个新列表,并将其作为调用者生成的列表使用”。我认为这是模棱两可的:
def x(a=datetime.datetime.now()):
用户,是否希望a默认为定义或执行x时对应的日期时间?在本例中,与前一例一样,我将保持与默认参数“赋值”是函数的第一条指令(函数调用时调用datetime.now())相同的行为。另一方面,如果用户想要定义时间映射,他可以写:
b = datetime.datetime.now()
def x(a=b):
我知道,我知道:这是一个结束。或者Python可以提供一个关键字来强制定义时间绑定:
def x(static a=b):
如果考虑到以下因素,这种行为并不奇怪:
尝试赋值时只读类属性的行为,以及函数是对象(在公认的答案中解释得很好)。
(2)的作用已在本主题中广泛讨论。(1) 很可能是令人惊讶的原因,因为这种行为在来自其他语言时并不“直观”。
(1) 在Python教程中对类进行了描述。尝试将值分配给只读类属性时:
…在最内部范围之外找到的所有变量都是只读(尝试写入这样的变量只会创建一个最内部范围中的新局部变量,保留相同的命名的外部变量保持不变)。
回顾最初的示例,并考虑以上几点:
def foo(a=[]):
a.append(5)
return a
这里foo是一个对象,a是foo的一个属性(在foo.func_defs[0]中可用)。由于a是一个列表,因此a是可变的,因此是foo读写属性。当函数实例化时,它被初始化为签名指定的空列表,并且只要函数对象存在,它就可用于读取和写入。
在不覆盖默认值的情况下调用foo使用foo.func_defs中的默认值。在这种情况下,foo.func_descfs[0]用于函数内对象的代码范围。更改foo.func_defs[0],它是foo对象的一部分,在执行foo中的代码之间持续存在。
现在,将其与文档中关于模拟其他语言的默认参数行为的示例进行比较,以便每次执行函数时都使用函数签名默认值:
def foo(a, L=None):
if L is None:
L = []
L.append(a)
return L
考虑到(1)和(2),可以看出为什么这会实现所需的行为:
当foo函数对象被实例化时,foo.func_defs[0]被设置为None,这是一个不可变的对象。当函数以默认值执行时(函数调用中没有为L指定参数),foo.func_defs[0](None)在本地作用域中可用为L。当L=[]时,foo.func_defs[0]处的赋值无法成功,因为该属性是只读的。根据(1),在局部作用域中创建一个新的局部变量(也称为L),并用于函数调用的其余部分。因此,对于未来的foo调用,foo.func_defs[0]保持不变。