【问题标题】:Why are default arguments evaluated at definition time?为什么在定义时评估默认参数?
【发布时间】:2010-12-11 16:34:53
【问题描述】:

我很难理解算法问题的根本原因。然后,通过逐步简化函数,我发现在 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 设计者决定在定义时评估默认参数?这对我来说似乎非常违反直觉。

【问题讨论】:

  • 我的猜测是性能。想象一下,如果每天调用 1500 万次,那么每次调用函数时都要重新评估。

标签: python logic language-design


【解决方案1】:

替代方案将是相当重量级的——在函数对象中存储“默认参数值”作为代码的“thunk”,每次调用函数时都会一遍又一遍地执行而没有为该参数指定值——并且会使获得早期绑定(在定义时间绑定)变得更加困难,这通常是您想要的。例如,在现有的 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 的默认值。

在这两种情况下强制后期绑定的替代方案不会提供这种灵活性,并且会在您每次需要早期绑定时强制您进行循环(例如将您的函数包装到闭包工厂),如上面的示例所示-- 这个假设的设计决策强加给程序员更多的重量级样板(除了在各处生成和反复评估thunk的“不可见”的样板)。

换句话说,“应该有一种,最好只有一种,明显的方式来做到这一点[1]”:当你想要后期绑定时,已经有一种非常明显的方式来实现它(因为所有函数的代码仅在调用时执行,显然 there 评估的所有内容都是后期绑定的);让 default-arg 评估产生早期绑定为您提供了一种实现早期绑定的明显方法(加号!-),而不是提供两种明显的方式来获得后期绑定并且没有明显的方式来获得早期绑定(减号!-)。

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

【讨论】:

  • 优秀的答案,来自我的 +1。一个非常小的错字:它应该是 return _memo[key] 并带有一个前导下划线。
  • @Francesco,tx 指出错字(我想 tx @novelocrat 如此迅速地修复它!-)。
  • 在深度复制而不是延迟评估的情况下,开销是否仍然令人望而却步?
【解决方案2】:

问题是这样的。

每次调用函数时都将函数评估为初始化程序太昂贵。

  • 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。哎呀。

【讨论】:

  • 我得到了“这将是昂贵的”部分,但“这是不可能的”部分我不明白。当有其他解释型动态语言可以做到这一点时,这并非不可能
【解决方案3】:

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

您还应该知道,在容器类型的情况下,可能会出现此问题——但您可以通过明确说明来规避它:

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

【讨论】:

  • 您也可以将其缩短为self.children = children or [],而不是使用 if 语句。
  • 如果我用 (children=None) 调用它会怎样。然后它会错误地创建children = []。为了解决这个问题,需要使用哨兵值。
  • 在这种情况下,我默默地假设 None 是一个合适的哨兵值。当然,如果 None 可能是一个有效值(在孩子的情况下(很可能是一系列事物)不太可能),则必须使用不同的哨兵值。如果不存在标准值,请为此使用专门创建的对象。
【解决方案4】:

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

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

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

【讨论】:

  • 嗨,Jed,当出现 [] 以外的输入时,可能会出现一些(罕见的)问题,评估结果为 False。然后合法的输入可能会转换为 []。当然,只要 chilren 必须是一个列表,就不会发生这种情况。
  • ... of forgot:更一般的说法是“如果 children 是 None ...”
  • “if children is None: children = []”(后面是“self.children = children”)是等价的(几乎---退化的值会不同)并且更具可读性。
  • @Juergen:我已经编辑了答案。 @R。佩特:可读性是相对的,我认为我的回答是相当可读的。
  • 它也可以写成self.children = children or [],假设您只希望列表为“子”,因此False 不是有效值。
【解决方案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()

【讨论】:

    【解决方案6】:

    这来自于 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

    【讨论】:

      【解决方案7】:

      我将通过添加其他帖子中的主要论点来提供不同意见。

      在函数执行时评估默认参数会影响性能。

      我觉得这很难相信。如果像foo='some_string' 这样的默认参数赋值确实增加了不可接受的开销,我相信可以识别​​不可变文字的赋值并预先计算它们。

      如果你想要一个带有可变对象的默认赋值,比如foo = [],只需在函数体中使用foo = None,然后是foo = foo or []

      虽然这在个别情况下可能没有问题,但作为一种设计模式,它并不是很优雅。它添加了样板代码并隐藏了默认参数值。如果 foo 可以是一个像一个具有未定义真值的 numpy 数组这样的对象,那么像 foo = foo or ... 这样的模式就不起作用。在None 是一个可以有意传递的有意义的参数值的情况下,它不能用作哨兵,这种解决方法变得非常丑陋。

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

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

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

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

      【讨论】:

        【解决方案8】:

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

        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
        

        因此,在评估该行代码的那一刻评估默认参数!

        【讨论】:

        • 不好的类比。与您的 java 示例等效的 pythonic 是将 if __name__ == '__main__': main() 插入到文件的末尾
        【解决方案9】:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-27
          • 1970-01-01
          • 2019-09-20
          • 1970-01-01
          • 2020-10-19
          相关资源
          最近更新 更多