为什么默认参数在定义时被评估?
我在理解一个算法问题的根本原因时遇到了很大的困难。然后,通过一步步简化函数,我发现Python中默认参数的评估方式并不像我预期的那样。
代码如下:
class Node(object):
def __init__(self, children = []):
self.children = children
问题在于,如果没有明确给出,Node类的每个实例都会共享同一个children
属性,比如:
>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176
我不太明白这种设计决策的逻辑是什么?为什么Python的设计者决定默认参数是在定义时进行评估的?这对我来说似乎很不直观。
9 个回答
当然,在你的情况下,这个问题很难理解。但是你要明白,每次都去计算默认参数会给系统带来很大的运行负担。
另外,你还应该知道,对于容器类型,这个问题可能会出现——不过你可以通过明确指定来避免这个问题:
def __init__(self, children = None):
if children is None:
children = []
self.children = children
问题是这样的。
每次调用一个函数时,如果都要重新计算这个函数作为初始值,那就太浪费资源了。
0
是一个简单的字面量。只需要计算一次,之后可以一直使用。int
是一个函数(就像列表一样),每次需要作为初始值时都得重新计算。
[]
这个构造是字面量,像 0
一样,表示“这个确切的对象”。
问题在于,有些人希望它能表示 list
,也就是说“请帮我计算这个函数,以获得作为初始值的对象”。
如果每次都要加上必要的 if
语句来进行这种计算,那将是一个巨大的负担。更好的做法是把所有参数都当作字面量,不在尝试进行函数计算时再进行额外的函数计算。
更根本地说,从技术上讲,作为函数计算的参数默认值是不可能实现的。
想象一下这种循环的可怕情况。假设我们允许默认值是函数,每次需要参数的默认值时都要计算这些函数。
[这和 collections.defaultdict
的工作方式类似。]
def aFunc( a=another_func ):
return a*2
def another_func( b=aFunc ):
return b*3
那么 another_func()
的值是什么呢?为了得到 b
的默认值,它必须计算 aFunc
,而这又需要计算 another_func
。哎呀。
另一种方法会比较复杂——把“默认参数值”存储在函数对象里,作为“代码块”,每次调用这个函数而没有指定参数值时,就要重复执行这些代码。这会让我们很难实现早绑定(在定义时绑定),而这通常是我们想要的。比如,在现在的Python中:
def ack(m, n, _memo={}):
key = m, n
if key not in _memo:
if m==0: v = n + 1
elif n==0: v = ack(m-1, 1)
else: v = ack(m-1, ack(m, n-1))
_memo[key] = v
return _memo[key]
...写一个像上面那样的记忆化函数是非常简单的。同样:
for i in range(len(buttons)):
buttons[i].onclick(lambda i=i: say('button %s', i))
...简单的 i=i
,依赖于默认参数值的早绑定(定义时绑定),是实现早绑定的一个非常简单的方法。所以,现在的规则很简单明了,让你以一种非常容易解释和理解的方式完成所有想做的事情:如果你想要表达式值的晚绑定,就在函数体内计算那个表达式;如果你想要早绑定,就把它作为参数的默认值来计算。
如果强制在这两种情况下都使用晚绑定,就不会提供这种灵活性,每次需要早绑定时,你都得费劲去处理(比如把你的函数放进一个闭包工厂里),就像上面的例子一样——这又是一个让程序员感到麻烦的设计决定(除了那些“看不见”的生成和重复评估代码块的麻烦)。
换句话说,“应该有一种,最好只有一种,明显的方法来做到这一点[1]”:当你想要晚绑定时,已经有一种非常明显的方法可以实现(因为函数的代码只有在调用时才会执行,显然在那里的所有计算都是晚绑定的);而默认参数值的计算产生早绑定也给你提供了一种明显的实现早绑定的方法(这是一种好处!),而不是给出两种明显的晚绑定方法却没有明显的早绑定方法(这是一种坏处!)。
[1]: “虽然这种方法一开始可能不明显,除非你是荷兰人。”