代码之家  ›  专栏  ›  技术社区  ›  Mert Nuhoglu

为什么python在定义时要计算默认参数?

  •  32
  • Mert Nuhoglu  · 技术社区  · 15 年前

    我很难理解算法中问题的根本原因。然后,通过一步一步地简化函数,我发现python中默认参数的求值并不像我期望的那样。

    代码如下:

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

    问题是节点类的每个实例共享相同的 children 属性,如果未显式给定该属性,例如:

    >>> n0 = Node()
    >>> n1 = Node()
    >>> id(n1.children)
    Out[0]: 25000176
    >>> id(n0.children)
    Out[0]: 25000176
    

    我不明白这个设计决定的逻辑?为什么python设计人员决定在定义时计算默认参数?这对我来说似乎很违反直觉。

    8 回复  |  直到 15 年前
        1
  •  38
  •   Phil Miller    15 年前

    另一种选择是非常重的——在函数对象中存储“默认参数值”作为代码的“重击”,以便在每次调用函数时都在没有为该参数指定值的情况下一遍又一遍地执行,这将使早期绑定(绑定)变得更加困难在定义时间),这通常是你想要的。例如,在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值的早期绑定(定义时间)是获得早期绑定的一种简单方法。所以,当前的规则是简单、直接的,并且允许您以一种非常容易解释和理解的方式来完成所有您想要的事情:如果您想要延迟绑定表达式的值,请在函数体中计算该表达式;如果您想要提前绑定,请将其作为精氨酸

    另一种选择是,在这两种情况下都强制延迟绑定,这并不能提供这种灵活性,并且会迫使您在每次需要提前绑定时都要经历一系列困难(比如将您的函数包装到一个闭包工厂中)。就像上面的例子一样——这个假设性的设计决策(除了生成和反复评估到处都是重击的“看不见的”决策)给程序员带来了更沉重的样板。

    换言之,“应该有一种,而且最好只有一种,显而易见的方法来实现它[1]:当您想要后期绑定时,已经有一种非常明显的方法来实现它(因为所有函数的代码都只在调用时执行,显然所有的东西都是经过计算的 那里 是后期绑定);使用默认的arg求值生成早期绑定也为您提供了一种实现早期绑定的明显方法(另外!-)而不是给两个明显的方式得到后期绑定和没有明显的方式得到早期绑定(A减!-)

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

        2
  •  10
  •   S.Lott    15 年前

    问题是这个。

    将函数作为初始值设定项求值太贵了 每次调用函数时 .

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

    • int 是一个函数(如list),每次需要它作为初始值设定项时都必须对其求值。

    结构 [] 是字面的,就像 ,意思是“这个确切的物体”。

    问题是有些人希望 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
  •  7
  •   Juergen    15 年前

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

    此外,您还应该知道,在容器类型的情况下,可能会出现此问题,但您可以通过显式处理来避免此问题:

    def __init__(self, children = None):
        if children is None:
           children = []
        self.children = children
    
        4
  •  7
  •   Jed Smith    15 年前

    解决办法, discussed here (非常坚实)是:

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

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

        5
  •  5
  •   Robert Rossney    15 年前

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

    函数是对象。在加载时,python创建函数对象,计算 def 语句,将它们放入元组中,并将该元组作为名为 func_defaults . 然后,当调用函数时,如果调用没有提供值,python将从 函数默认值 .

    例如:

    >>> 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
  •  4
  •   jcdyer Anand S Kumar    15 年前

    这来自于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
  •  0
  •   Jonathan Feinberg    15 年前

    python函数定义只是代码,与所有其他代码一样;它们不像某些语言那样“神奇”。例如,在java中,可以将“now”指代为“later”定义的内容:

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

    但在巨蟒中

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

    所以,默认参数是在对代码行求值时计算的!

        8
  •  0
  •   fortran    15 年前

    因为如果有的话,有人会发帖问为什么不是相反的方式-p

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

    推荐文章