“最小的惊讶”和可变的默认论证-python

任何修补Python足够长的人都被以下问题咬伤(或撕成碎片):


def foo(a=[]):

    a.append(5)

    return a

Python新手希望这个函数总能返回一个只包含一个元素的列表:[5]。结果却非常不同,而且非常惊人(对于新手来说):


>>> foo()

[5]

>>> foo()

[5, 5]

>>> foo()

[5, 5, 5]

>>> foo()

[5, 5, 5, 5]

>>> foo()

我的一位经理曾经第一次遇到这个功能,并称其为该语言的“戏剧性设计缺陷”。我回答说这个行为有一个潜在的解释,如果你不理解内部,那确实非常令人费解和意想不到。但是,我无法回答(对自己)以下问题:在函数定义中绑定默认参数的原因是什么,而不是在函数执行时?我怀疑经验丰富的行为有实际用途(谁真的在C中使用静态变量,没有繁殖错误?)


编辑:


巴泽克提出了一个有趣的例子。再加上你的大部分评论和特别是Utaal,我进一步阐述了:


>>> def a():

...     print("a executed")

...     return []

... 

>>>            

>>> def b(x=a()):

...     x.append(5)

...     print(x)

... 

a executed

>>> b()

[5]

>>> b()

[5, 5]

对我而言,似乎设计决策是相对于放置参数范围的位置:在函数内部还是“与它一起”?


在函数内部进行绑定意味着在调用函数时x有效地绑定到指定的默认值,而不是定义,这会产生一个深层次的缺陷:该def行在某种意义上是“混合”的(部分绑定)函数对象)将在定义时发生,并在函数调用时发生部分(默认参数的赋值)。


实际行为更加一致:执行该行时,该行的所有内容都会得到评估,这意味着在函数定义中。


炎炎设计
浏览 498回答 6
6回答

侃侃尔雅

实际上,这不是设计缺陷,并不是因为内部或性能。它只是因为Python中的函数是第一类对象,而不仅仅是一段代码。一旦你以这种方式思考,那么它就完全有意义了:一个函数是一个被定义的对象; 默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改为另一个调用 - 与任何其他对象完全相同。无论如何,Effbot 对Python中的默认参数值中出现这种行为的原因有一个非常好的解释。我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。

慕沐林林

假设您有以下代码fruits = ("apples", "bananas", "loganberries")def eat(food=fruits):&nbsp; &nbsp; ...当我看到吃的声明时,最令人惊讶的是认为如果没有给出第一个参数,它将等于元组 ("apples", "bananas", "loganberries")但是,假设后面的代码,我会做类似的事情def some_random_function():&nbsp; &nbsp; global fruits&nbsp; &nbsp; fruits = ("blueberries", "mangos")然后,如果默认参数在函数执行而不是函数声明中被绑定,那么我会惊讶地发现水果已被改变(以非常糟糕的方式)。这比发现foo上面的函数改变列表更令人惊讶的IMO 。真正的问题在于可变变量,并且所有语言在某种程度上都存在这个问题。这是一个问题:假设在Java中我有以下代码:StringBuffer s = new StringBuffer("Hello World!");Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();counts.put(s, 5);s.append("!!!!");System.out.println( counts.get(s) );&nbsp; // does this work?现在,我的地图StringBuffer在放入地图时是否使用了键的值,还是通过引用存储了键?无论哪种方式,有人感到惊讶; 尝试将对象从Map使用中取出的值与他们放入的对象相同的人,或者即使他们使用的键实际上是同一个对象而无法检索其对象的人用于将其放入映射的(这实际上是Python不允许其可变内置数据类型用作字典键的原因)。你的例子是一个很好的例子,Python新人会感到惊讶和被咬。但我认为,如果我们“修复”这个,那么这只会产生一种不同的情况,即他们会被咬伤,而那种情况甚至会更不直观。而且,在处理可变变量时总是如此; 你总是遇到一些情况,根据他们正在编写的代码,某人可能直观地期望一种或相反的行为。我个人喜欢Python当前的方法:默认函数参数在定义函数时进行评估,并且该对象始终是默认值。我想他们可以使用空列表进行特殊情况,但这种特殊的外壳会引起更多的惊讶,更不用说倒退不兼容了

杨魅力

AFAICS尚未发布文档的相关部分:执行函数定义时,将评估默认参数值。这意味着当定义函数时,表达式被计算一次,并且每次调用使用相同的“预先计算”值。这对于理解默认参数是可变对象(例如列表或字典)时尤其重要:如果函数修改对象(例如,通过将项附加到列表),则默认值实际上被修改。这通常不是预期的。解决这个问题的方法是使用None作为默认值,并在函数体中显式测试它[...]

尚方宝剑之说

我对Python解释器内部工作一无所知(我也不是编译器和解释器方面的专家)所以如果我提出任何不可知或不可能的建议,请不要责怪我。如果python对象是可变的,我认为在设计默认参数时应该考虑到这一点。实例化列表时:a&nbsp;=&nbsp;[]你希望得到一个新的列表引用a。为什么要a=[]进去def&nbsp;x(a=[]):在函数定义上实例化一个新列表而不是在调用上?就像你问“用户是否提供参数然后实例化一个新列表并使用它就好像它是由调用者生成”一样。我认为这是模棱两可的:def&nbsp;x(a=datetime.datetime.now()):用户,是否要a默认为与定义或执行时相对应的日期时间x?在这种情况下,与前一个一样,我将保持相同的行为,就好像默认参数“assignment”是函数的第一条指令(datetime.now()在函数调用上调用)。另一方面,如果用户想要定义时间映射,他可以写:b&nbsp;=&nbsp;datetime.datetime.now()def&nbsp;x(a=b):我知道,我知道:这是一个封闭。或者,Python可能会提供一个关键字来强制定义时绑定:def&nbsp;x(static&nbsp;a=b):

隔江千里

嗯,原因很简单,在执行代码时完成绑定,并且执行函数定义,以及......定义函数时。比较一下:class&nbsp;BananaBunch: &nbsp;&nbsp;&nbsp;&nbsp;bananas&nbsp;=&nbsp;[] &nbsp;&nbsp;&nbsp;&nbsp;def&nbsp;addBanana(self,&nbsp;banana): &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;self.bananas.append(banana)此代码遭受完全相同的意外事件。bananas是一个类属性,因此,当您向其添加内容时,它会添加到该类的所有实例中。原因完全一样。这只是“如何工作”,并且在功能案例中使其工作方式可能很复杂,并且在类的情况下可能不可能,或者至少减慢对象实例化的速度,因为你必须保持类代码并在创建对象时执行它。是的,这是出乎意料的。但是一旦下降了,它就完全适合Python的工作方式。事实上,它是一个很好的教学辅助工具,一旦你理解了为什么会这样,你就会更好地理解python。这说它应该在任何优秀的Python教程中占据突出地位。因为正如你所提到的,每个人迟早都会遇到这个问题。

繁星coding

我曾经认为在运行时创建对象将是更好的方法。我现在不太确定,因为你确实失去了一些有用的功能,尽管它可能是值得的,不管只是为了防止新手混淆。这样做的缺点是:1.表现def foo(arg=something_expensive_to_compute())):&nbsp; &nbsp; ...如果使用了调用时评估,则每次使用函数时都会调用昂贵的函数而不使用参数。您要么为每次调用付出昂贵的代价,要么需要在外部手动缓存该值,污染您的命名空间并添加详细程度。2.强制绑定参数一个有用的技巧是在创建lambda时将lambda的参数绑定到变量的当前绑定。例如:funcs = [ lambda i=i: i for i in range(10)]这将返回分别返回0,1,2,3 ...的函数列表。如果行为发生了变化,它们将绑定i到i 的调用时间值,因此您将获得所有返回的函数列表9。否则实现此方法的唯一方法是使用i绑定创建进一步的闭包,即:def make_func(i): return lambda: ifuncs = [make_func(i) for i in range(10)]3.内省考虑一下代码:def foo(a='test', b=100, c=[]):&nbsp; &nbsp;print a,b,c我们可以使用inspect模块获取有关参数和默认值的信息>>> inspect.getargspec(foo)(['a', 'b', 'c'], None, None, ('test', 100, []))这些信息对于文档生成,元编程,装饰器等非常有用。现在,假设可以更改默认值的行为,以便这相当于:_undefined = object()&nbsp; # sentinel valuedef foo(a=_undefined, b=_undefined, c=_undefined)&nbsp; &nbsp; if a is _undefined: a='test'&nbsp; &nbsp; if b is _undefined: b=100&nbsp; &nbsp; if c is _undefined: c=[]但是,我们已经失去了内省的能力,并且看到了默认参数是什么。因为没有构造对象,所以我们不能在没有实际调用函数的情况下获取它们。我们能做的最好的事情是存储源代码并将其作为字符串返回。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Python