为什么在 Python 中定义时要评估默认参数?
-
22-07-2019 - |
题
我很难理解算法中问题的根本原因。然后,通过一步步简化函数,我发现 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 设计者决定在定义时评估默认参数?这对我来说似乎非常违反直觉。
解决方案
在替代方案是相当重量级 - 在函数对象存储“默认参数值”为代码“的thunk”每函数被调用,而不用于该参数指定值的时间被反复执行再次 - 和将使其更难获得早期绑定(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]
...写像上面一个memoized功能的相当基本的任务。类似地:
for i in range(len(buttons)):
buttons[i].onclick(lambda i=i: say('button %s', i))
...简单i=i
,依靠默认ARG值的早期结合(定义时间),是一个平凡简单的方式来获得早期绑定。因此,目前的规则很简单,直接,让你做你的方式,是非常容易解释和理解想:如果你想后期绑定表达式的值,评估函数体中的表达;如果你想早期绑定,评估它作为一个arg的默认值。
在选择,迫使两种情况后期绑定,不会提供这种灵活性,并会迫使你去赴汤蹈火(如包装您的功能为关闭工厂)每当你需要早期绑定时,如上面的例子 - 还更重量级样板由该假想的设计决策(超出生成和反复评估的thunk所有的地方的“不可见”的)被迫对程序员
在换句话说,“应该有一个,最好是只有一种,明显的方法来做到这一点[1]”:当你想后期绑定,还有已经达到了它(因为所有的功能代码的一个十分明显的方式仅在呼叫时执行的,显然一切评价的有信息是后期绑定);其默认ARG评价产生早期绑定为您提供了一个明显的方式来实现早期绑定以及(加 - !),而不是给两个明显的方式来获得后期绑定,并没有明显的方式来获得早期绑定(减号 - !)。
[1]:“尽管除非你荷兰人这种方式可能不会在第一是明显的”
其他提示
问题是这样的。
将函数作为初始值设定项进行评估的成本太高 每次调用该函数时.
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
. 。哎呀。
当然,在您的情况是很难理解的。不过你要明白,每一次评估默认ARGS将系统放在一个沉重的负担,运行时间
您也应该知道,在容器类型的情况下,可能会出现这样的问题 - 但是你可以通过使事情明确绕过它:
def __init__(self, children = None):
if children is None:
children = []
self.children = children
这种情况的解决方法,此处(和非常讨论固体),是:
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
的。
例如:
>>> 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()
这来自于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
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
因此,默认参数是在时刻,该行代码被评估评估!
因为如果他们有,那么会有人张贴问题,询问为何不在身边:-P其他方式
现在假设他们有。你会如何,如果需要执行当前的行为?这很容易在函数内部创建新的对象,但你不能“uncreate”他们(你可以将其删除,但它是不一样的)。
我将提供一个反对意见,通过在其他职位addessing主要论点。
评估默认参数时被执行的功能将是不好的性能。
我觉得这很难相信。如果像foo='some_string'
默认参数赋值真正增加开销的不可接受的量,我敢肯定,这将有可能确定分配给一成不变的文字和预先计算它们。
如果你想与像
foo = []
可变对象缺省分配,只要使用foo = None
,其次是foo = foo or []
函数体。
虽然这可能是在个别情况下没有问题,作为一个设计图案它不是很优雅。它增加了样板代码和掩盖默认参数值。像foo = foo or ...
模式不工作,如果foo
能像未定义真值numpy的数组的对象。并且在None
是可以有意地通过一个有意义的参数值的情况下,它不能被用来作为一个定点和此解决方法变得很难看。
在当前行为是该可变缺省对象有用应该翻过函数调用共享。
我会很高兴地看到相反的证据,但在我的经验,这种使用情况比应重新每次函数被调用时创建可变对象频繁得多。对我来说,也似乎是一个更高级的使用情况,而空货柜意外默认分配是对新Python程序员一个常见的问题。因此,最小惊讶的原理表明在执行功能时默认参数值进行评估。
此外,我认为存在用于可变对象简单的解决方法应该在函数共享调用:初始化它们的功能外
所以,我认为这是一个不好的设计决策。我的猜测是,它被选中,是因为它的实现实际上是简单,因为它有一个有效的(尽管有限)的使用情况。不幸的是,我不认为这永远不会改变,因为核心Python开发者想要避免了Python 3引入了向后不兼容性量的重复。