关于逻辑:为什么在Python的定义时间评估默认参数?

Why are default arguments evaluated at definition time in Python?

我很难理解算法中问题的根本原因。然后,通过一步一步地简化函数,我发现在Python中对默认参数的计算并不像我预期的那样工作。

代码如下:

1
2
3
class Node(object):
    def __init__(self, children = []):
        self.children = children

问题是,如果没有显式地给出属性,则节点类的每个实例都共享相同的children属性,例如:

1
2
3
4
5
6
>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176

我不明白这个设计决策的逻辑?为什么Python设计人员决定在定义时评估默认参数?这对我来说似乎是非常违反直觉的。


另一种选择是非常重要的——在函数对象中存储"默认参数值"作为代码的"thunks",每次调用函数时都会反复执行该参数而没有指定的值——这会使早期绑定(在def时绑定)变得更加困难,而这通常是您想要的。例如,在现有的python中:

1
2
3
4
5
6
7
8
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]

…写一个像上面这样的记忆函数是一项非常基本的任务。类似地:

1
2
for i in range(len(buttons)):
  buttons[i].onclick(lambda i=i: say('button %s', i))

…简单的i=i依赖于默认arg值的早期绑定(定义时间),是获得早期绑定的非常简单的方法。所以,当前的规则很简单、简单,并且让您以一种非常容易解释和理解的方式来做您想要做的所有事情:如果您想要一个表达式值的后期绑定,请在函数体中评估该表达式;如果您想要早期绑定,请将其评估为arg的默认值。

对于这两种情况,强制延迟绑定的替代方法并不能提供这种灵活性,并且会迫使您在每次需要提前绑定时(如将函数包装到一个关闭工厂中)都经历一个循环,如上面的示例所示——然而,这种假设的设计决策迫使程序员使用更重的样板文件(除了"看不见的"之外,到处都在产生和反复评估雷声)。

换句话说,"应该有一种,最好只有一种,显而易见的方法来完成它[1]":当您想要延迟绑定时,已经有了一种非常明显的方法来实现它(因为所有函数的代码都只在调用时执行,显然所有被评估的东西都有延迟绑定);让默认的arg评估产生早期绑定会给您带来你也是一个很明显的实现早期绑定的方法(一个优点!-)而不是给出两个明显的方法来进行后期绑定,而没有明显的方法来进行早期绑定(A减!-)

【1】:"尽管一开始这种方式可能并不明显,除非你是荷兰语。"


问题是这个。

每次调用函数时,将函数作为初始值设定项进行计算太昂贵了。

  • 0是一个简单的文字。一次评估,永远使用。

  • int是一个函数(类似于列表),每次需要它作为初始值设定项时都必须对其进行评估。

构造[]0一样是文字,意思是"这个确切的对象"。

问题是,有些人希望它的意思是list,如"请为我评估这个函数,以获得初始化器的对象"。

要一直进行这种评估,增加必要的if声明将是一个沉重的负担。最好将所有参数作为文本,不要在尝试进行函数评估时进行任何额外的函数评估。

而且,从根本上讲,在技术上不可能实现参数默认值作为函数评估。

考虑一下这种循环的递归恐惧。假设默认值不是文本,而是允许它们是每次需要参数的默认值时评估的函数。

[这将与collections.defaultdict的工作方式相平行。]

1
2
3
4
5
def aFunc( a=another_func ):
    return a*2

def another_func( b=aFunc ):
    return b*3

another_func()的价值是多少?为了得到b的默认值,它必须对aFunc进行评估,这需要对another_func进行评估。哎呀。


当然,在你的情况下,很难理解。但是您必须看到,每次评估默认参数都会给系统带来沉重的运行时负担。

另外,您应该知道,在容器类型的情况下,这个问题可能会发生——但是您可以通过使事情明确化来绕过它:

1
2
3
4
def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children


这里讨论的解决方法(非常可靠)是:

1
2
3
class Node(object):
    def __init__(self, children = None):
        self.children = [] if children is None else children

为什么要找冯·L的答案?WIS,但这很可能是因为函数定义根据python的体系结构生成了一个代码对象,并且在默认参数中可能没有一个处理类似这样的引用类型的工具。


我认为这也是违反直觉的,直到我了解了Python如何实现默认参数。

函数是一个对象。在加载时,python创建函数对象,计算def语句中的默认值,将其放入元组中,并将该元组添加为名为func_defaults的函数的属性。然后,在调用函数时,如果调用不提供值,则python将从func_defaults中获取默认值。

例如:

1
2
3
4
5
6
7
8
>>> class C():
        pass

>>> def f(x=C()):
        pass

>>> f.func_defaults
(<__main__.C instance at 0x0298D4B8>,)

所以所有对f的调用,如果不提供参数,将使用C的相同实例,因为这是默认值。

至于为什么python这样做:好吧,这个tuple可以包含每次需要默认参数值时都会被调用的函数。除了立即明显的性能问题外,您开始进入一个特殊情况的世界,比如为不可变类型存储文本值而不是函数,以避免不必要的函数调用。当然,对性能的影响也很大。

实际的行为非常简单。还有一个简单的解决方法,在您希望在运行时通过函数调用生成默认值的情况下:

1
2
3
def f(x = None):
   if x == None:
      x = g()

这来自于Python对语法和执行简单性的强调。def语句在执行期间的某一点发生。当python解释器到达该点时,它计算该行中的代码,然后从函数体创建一个代码对象,稍后调用函数时将运行该对象。

它是函数声明和函数体之间的简单划分。声明在代码中到达时执行。主体在调用时执行。请注意,声明每次到达时都会执行,因此您可以通过循环创建多个函数。

1
2
3
4
5
6
7
8
9
funcs = []
for x in xrange(5):
    def foo(x=x, lst=[]):
        lst.append(x)
        return lst
    funcs.append(foo)
for func in funcs:
    print"1:", func()
    print"2:", func()

已经创建了五个单独的函数,每次执行函数声明时都会创建一个单独的列表。在通过funcs的每个循环上,相同的函数在每个传递上执行两次,每次使用相同的列表。结果如下:

1
2
3
4
5
6
7
8
9
10
1:  [0]
2:  [0, 0]
1:  [1]
2:  [1, 1]
1:  [2]
2:  [2, 2]
1:  [3]
2:  [3, 3]
1:  [4]
2:  [4, 4]

其他人提供了解决方法,使用param=none,如果值为none,则在正文中指定一个列表,这完全是惯用的python。它有点难看,但是简单性很强,而且解决方法也不太痛苦。

编辑后添加:有关此问题的详细讨论,请参阅effbot的文章:http://effbot.org/zone/default-values.htm,以及语言引用:http://docs.python.org/reference/compound-stmts.html函数


因为如果他们有,那么会有人发问为什么不是相反的方式。

假设现在他们有了。如果需要,您将如何实现当前行为?在函数中创建新对象很容易,但不能"取消创建"它们(可以删除它们,但不同)。


与其他所有代码一样,Python函数定义只是代码;它们不像某些语言那样具有"魔力"。例如,在Java中,你可以把"现在"定义为"稍后"的定义:

1
2
3
public static void foo() { bar(); }
public static void main(String[] args) { foo(); }
public static void bar() {}

但在Python中

1
2
3
4
def foo(): bar()
foo()   # boom!"bar" has no binding yet
def bar(): pass
foo()   # ok

因此,在对该行代码进行计算时,将对默认参数进行计算!