任何人只要长时间摆弄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行将是“混合”的,即部分绑定(函数对象)将在定义时发生,部分绑定(默认参数的赋值)将在函数调用时发生。
实际行为更加一致:当执行该行时,该行的所有内容都会得到求值,这意味着在函数定义时。
TLDR:定义时间默认值是一致的,严格来说更具表达力。
定义函数会影响两个作用域:包含函数的定义作用域和函数所包含的执行作用域。虽然很清楚块是如何映射到作用域的,但问题是def<name>(<args=defaults>):属于:
... # defining scope
def name(parameter=default): # ???
... # execution scope
def-name部分必须在定义范围内求值,毕竟我们希望name在定义范围中可用。仅在函数内部求值将使其无法访问。
由于参数是一个常量名称,所以我们可以在定义名称的同时对其进行“求值”。这还有一个优点,它生成的函数具有已知签名name(parameter=…):,而不是裸名(…):。
现在,何时评估默认值?
一致性已经表明“在定义时”:def<name>(<args=defaults>)的所有其他属性:也最好在定义时进行评估。推迟部分时间将是一个令人惊讶的选择。
这两种选择也不等同:如果在定义时计算默认值,它仍然会影响执行时间。如果在执行时计算默认值,则不会影响定义时间。选择“at definition”可以表达两种情况,而选择“at executing”只能表达一种情况:
def name(parameter=defined): # set default at definition time
...
def name(parameter=default): # delay default until execution time
parameter = default if parameter is None else parameter
...
我将演示一种将默认列表值传递给函数的替代结构(它与字典同样适用)。
正如其他人广泛评论的那样,列表参数在定义时绑定到函数,而不是在执行时。由于列表和字典是可变的,因此对该参数的任何更改都将影响对该函数的其他调用。因此,对函数的后续调用将接收此共享列表,该列表可能已被对函数的任何其他调用更改。更糟糕的是,两个参数同时使用该函数的共享参数,而忽略了另一个参数所做的更改。
错误的方法(可能…):
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]
“错误方法”可能有合法的用例,程序员希望共享默认列表参数,但这更可能是例外而不是规则。
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