为什么在Python的定义时间评估默认参数?

时间:2009-10-30 17:16:08

标签: python logic language-design

我很难理解算法中问题的根本原因。然后,通过逐步简化函数,我发现在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 个答案:

答案 0 :(得分:38)

替代方案将是非常重量级 - 在函数对象中存储“默认参数值”作为代码的“thunks”,每次调用函数时都会反复执行该参数而没有指定的参数值 - 和会使得早期绑定(在def时间绑定)变得更加困难,这通常是你想要的。例如,在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,依赖于默认arg值的早期绑定(定义时间),是一种简单的方法来获得早期绑定。因此,当前规则简单,直接,并且允许您以非常容易解释和理解的方式执行所有操作:如果您希望对表达式的值进行后期绑定,请在函数体中评估该表达式;如果您想要早期绑定,请将其评估为arg的默认值。

替代方案,强制两种情况的后期绑定,不会提供这种灵活性,并且会在每次需要早期绑定时强制您通过环(例如将函数包装到闭包工厂),如上例所示 - 通过这个假设的设计决策强迫程序员使用更重量级的样板(超出“无形”产生并反复评估整个地方的thunk)。

换句话说,“应该有一个,最好只有一个,显而易见的方法[1]”:当你想要后期绑定时,已经有一种非常明显的方法来实现它(因为所有函数的代码都是如此)仅在通话时执行,显然所有评估那里都是后期绑定的);使用default-arg评估产生早期绑定为你提供了一种明显的方法来实现早期绑定(加上! - ),而不是给出两种明显的方法来获得后期绑定,而没有明显的方法来获得早期绑定(减去! - )。

[1]:“虽然这种方式起初可能并不明显,除非你是荷兰人。”

答案 1 :(得分:10)

问题是这个。

每次调用函数时,将函数计算为初始化函数太昂贵了。

  • 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的eval。糟糕。

答案 2 :(得分:7)

此问题的解决方法discussed here(非常可靠)是:

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

至于为什么要从vonLöwis寻找答案,但可能是因为函数定义由于Python的体系结构而产生代码对象,并且可能没有在默认参数中使用这样的引用类型的工具。

答案 3 :(得分:7)

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

另外你应该知道,在容器类型的情况下可能会出现这个问题 - 但是你可以通过使事物明确来规避它:

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children

答案 4 :(得分:5)

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

一个函数是一个对象。在加载时,Python创建函数对象,评估def语句中的默认值,将它们放入元组中,并将该元组添加为名为func_defaults的函数的属性。然后,当调用函数时,如果调用没有提供值,Python会从func_defaults中删除默认值。

例如:

>>> class C():
        pass

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

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

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

就Python为什么这样做:好吧,元组可能包含每次需要默认参数值时都会被调用的函数。除了明显的性能问题之外,您还开始涉及特殊情况,例如存储文字值而不是非可变类型的函数,以避免不必要的函数调用。当然,还有很多性能影响。

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

def f(x = None):
   if x == None:
      x = g()

答案 5 :(得分:4)

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

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

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:  [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#function

答案 6 :(得分:0)

Python函数定义只是代码,就像所有其他代码一样;它们不像某些语言那样“神奇”。例如,在Java中,您可以将“now”引用为“稍后”定义的内容:

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

但是在Python中

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

因此,在评估该行代码时评估默认参数!

答案 7 :(得分:0)

因为如果他们有,那么有人会发一个问题,问为什么不相反:-p

现在假设他们有。如果需要,您将如何实现当前行为?在函数内部创建新对象很容易,但是你不能“取消”它们(你可以删除它们,但它们不一样)。

答案 8 :(得分:0)

在其他文章中加入主要论点,我将提出不同意见。

  

在执行函数时评估默认参数会降低性能。

我很难相信这一点。如果像foo='some_string'这样的默认参数分配确实增加了不可接受的开销,我相信可以确定不可变文字的分配并对其进行预先计算。

  

如果要使用foo = []之类的可变对象进行默认赋值,只需使用foo = None,然后在函数正文中使用foo = foo or []

尽管在个别情况下这可能没有问题,但作为一种设计模式,它并不是很优雅。它添加样板代码并掩盖默认参数值。如果foo = foo or ...可以是具有未定义真值的numpy数组之类的对象,则foo之类的模式将不起作用。并且在None是可能会有意传递的有意义的参数值的情况下,它不能用作标记,并且此变通方法会变得非常丑陋。

  

当前行为对于应该在函数调用中共享的可变默认对象很有用。

我很高兴看到相反的证据,但是根据我的经验,这种用例的频率要比每次调用该函数都应重新创建的可变对象要少得多。在我看来,这似乎是一个更高级的用例,而空容器的意外默认分配是新Python程序员的常见难题。因此,最小惊讶原则建议在执行函数时应评估默认参数值。

另外,在我看来,对于可变对象应该有一个简单的解决方法,该可变对象应在函数调用之间共享:在函数外部初始化它们。

因此,我认为这是一个错误的设计决定。我的猜测是之所以选择它,是因为它的实现实际上更简单并且因为它具有有效的(尽管有限)用例。不幸的是,我认为这不会改变,因为Python的核心开发人员希望避免重复出现Python 3引入的向后不兼容的情况。