任何人只要长时间摆弄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 print_tuple(some_tuple=(1,2,3)):
print some_tuple
print_tuple() #1
print_tuple((1,2,3)) #2
我会给你一个提示。这是拆卸(参见http://docs.python.org/library/dis.html):
#1
0 LOAD_GLOBAL 0 (print_tuple)
3 CALL_FUNCTION 0
6 POP_TOP
7 LOAD_CONST 0 (None)
10 RETURN_VALUE
#2
0 LOAD_GLOBAL 0 (print_tuple)
3 LOAD_CONST 4 ((1, 2, 3))
6 CALL_FUNCTION 1
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
我怀疑有经验的行为是否有实际用途(谁真的在C中使用了静态变量,而没有滋生bug?)
正如您所看到的,使用不可变的默认参数会带来性能上的好处。如果它是一个频繁调用的函数,或者默认参数需要很长时间才能构造,那么这可能会有所不同。此外,请记住Python不是C。在C中,您可以使用非常免费的常量。在Python中,你没有这个好处。
这是一种性能优化。由于此功能,您认为这两个函数调用中哪一个更快?
def print_tuple(some_tuple=(1,2,3)):
print some_tuple
print_tuple() #1
print_tuple((1,2,3)) #2
我会给你一个提示。这是拆卸(参见http://docs.python.org/library/dis.html):
#1
0 LOAD_GLOBAL 0 (print_tuple)
3 CALL_FUNCTION 0
6 POP_TOP
7 LOAD_CONST 0 (None)
10 RETURN_VALUE
#2
0 LOAD_GLOBAL 0 (print_tuple)
3 LOAD_CONST 4 ((1, 2, 3))
6 CALL_FUNCTION 1
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
我怀疑有经验的行为是否有实际用途(谁真的在C中使用了静态变量,而没有滋生bug?)
正如您所看到的,使用不可变的默认参数会带来性能上的好处。如果它是一个频繁调用的函数,或者默认参数需要很长时间才能构造,那么这可能会有所不同。此外,请记住Python不是C。在C中,您可以使用非常免费的常量。在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])
可以调整装饰器以允许这一点,但我们将此作为读者的练习;)
我过去认为在运行时创建对象是更好的方法。我现在不太确定,因为你确实失去了一些有用的功能,尽管这可能是值得的,无论是为了防止新手混淆。这样做的缺点是:
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=[]
然而,我们已经失去了自省的能力,无法看到默认参数是什么。因为对象还没有被构造,所以我们无法在不调用函数的情况下获取它们。我们所能做的最好的方法是存储源代码并将其作为字符串返回。