问题 “最小的惊讶”和可变的默认论证
任何修补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中使用静态变量,没有繁殖错误?)
编辑:
Baczek做了一个有趣的例子。再加上你的大部分评论和尤其是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
对于(函数对象的)绑定的一部分将在定义时发生,并且在函数调用时发生部分(默认参数的赋值),行将是“混合”。
实际行为更加一致:执行该行时,该行的所有内容都会得到评估,这意味着在函数定义中。
12022
2017-07-15 18:00
起源
答案:
实际上,这不是设计缺陷,并不是因为内部或性能。
它只是因为Python中的函数是第一类对象,而不仅仅是一段代码。
一旦你以这种方式思考,那么它就完全有意义了:一个函数是一个被定义的对象;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改为另一个调用 - 与任何其他对象完全相同。
无论如何,Effbot对这种行为的原因有一个非常好的解释 Python中的默认参数值。
我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。
1353
2017-07-17 21:29
假设您有以下代码
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
当我看到吃的声明时,最令人惊讶的是认为如果没有给出第一个参数,它将等于元组 ("apples", "bananas", "loganberries")
但是,假设后面的代码,我会做类似的事情
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
然后,如果默认参数在函数执行而不是函数声明中被绑定,那么我会惊讶地发现水果已被改变(以非常糟糕的方式)。这将是比发现你的更令人惊讶的IMO foo
上面的函数正在改变列表。
真正的问题在于可变变量,并且所有语言都在某种程度上存在这个问题。这是一个问题:假设在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) ); // does this work?
现在,我的地图是否使用了该值 StringBuffer
将密钥放入地图时,还是通过引用存储密钥?无论哪种方式,有人都感到惊讶;试图将物体从中取出的人 Map
使用与他们放入的值相同的值,或者即使他们使用的键实际上是用于将其放入地图的相同对象,也无法检索其对象的人(这是实际上为什么Python不允许将其可变内置数据类型用作字典键。
你的例子是一个很好的例子,Python新人会感到惊讶和被咬。但是我认为,如果我们“修复”了这个问题,那么这只会产生一种不同的情况,即他们会被咬住,而这种情况甚至会更不直观。而且,在处理可变变量时总是如此;你总是遇到一些情况,根据他们正在编写的代码,某人可能直观地期望一种或相反的行为。
我个人喜欢Python当前的方法:在定义函数时评估默认函数参数,并且该对象始终是默认值。我想他们可以使用空列表进行特殊情况,但这种特殊的外壳会引起更多的惊讶,更不用说倒退不兼容了。
231
2017-07-15 18:11
AFAICS还没有人发布相关部分 文件:
执行函数定义时,将评估默认参数值。 这意味着在定义函数时,表达式被计算一次,并且每个调用使用相同的“预先计算”值。这对于理解默认参数是可变对象(例如列表或字典)时尤其重要:如果函数修改对象(例如,通过将项附加到列表),则默认值实际上被修改。这通常不是预期的。解决这个问题的方法是使用None作为默认值,并在函数体中显式测试它[...]
195
2017-07-10 14:50
我对Python解释器内部工作一无所知(我也不是编译器和解释器方面的专家)所以如果我提出任何不可知或不可能的建议,不要怪我。
提供python对象 是可变的 我认为在设计默认参数时应该考虑到这一点。
实例化列表时:
a = []
你希望得到一个 新 列表引用 一个。
为什么a = [] in
def x(a=[]):
在函数定义上实例化一个新列表而不是在调用上?
这就像你问“用户是否不提供参数 实例 一个新的列表,并使用它,就好像它是由调用者生成的“。
我认为这是模棱两可的:
def x(a=datetime.datetime.now()):
用户,你想要吗? 一个 默认为与您定义或执行时相对应的日期时间 X?
在这种情况下,与前一个一样,我将保持相同的行为,就好像默认参数“assignment”是函数的第一条指令(在函数调用上调用datetime.now())。
另一方面,如果用户想要定义时间映射,他可以写:
b = datetime.datetime.now()
def x(a=b):
我知道,我知道:这是一个封闭。或者,Python可能会提供一个关键字来强制定义时绑定:
def x(static a=b):
97
2017-07-15 23:21
嗯,原因很简单,在执行代码时完成绑定,并且执行函数定义,以及......定义函数时。
比较一下:
class BananaBunch:
bananas = []
def addBanana(self, banana):
self.bananas.append(banana)
此代码遭受完全相同的意外事件。 bananas是一个类属性,因此,当您向其添加内容时,它会添加到该类的所有实例中。原因完全一样。
它只是“如何工作”,并且在功能案例中使其工作方式可能很复杂,并且在类的情况下可能不可能,或者至少减慢对象实例化的速度,因为你必须保持类代码并在创建对象时执行它。
是的,这是出乎意料的。但是一旦便士下降,它就完全适合Python的工作方式。事实上,它是一个很好的教学辅助工具,一旦你理解了为什么会发生这种情况,你就会更好地理解python。
这说它应该在任何优秀的Python教程中突出显示。因为正如你所提到的,每个人迟早都会遇到这个问题。
72
2017-07-15 18:54
我曾经认为在运行时创建对象将是更好的方法。我现在不太确定,因为你确实失去了一些有用的功能,尽管它可能是值得的,不管只是为了防止新手混淆。这样做的缺点是:
1.表现
def foo(arg=something_expensive_to_compute())):
...
如果使用了调用时评估,则每次使用函数时都会调用昂贵的函数而不使用参数。您要么为每次调用付出昂贵的代价,要么需要在外部手动缓存该值,污染您的命名空间并添加详细程度。
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: i
funcs = [make_func(i) for i in range(10)]
3.内省
考虑一下代码:
def foo(a='test', b=100, c=[]):
print a,b,c
我们可以使用。获取有关参数和默认值的信息 inspect
模块,哪个
>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))
此信息对于文档生成,元编程,装饰器等非常有用。
现在,假设可以更改默认值的行为,以便这相当于:
_undefined = object() # sentinel value
def foo(a=_undefined, b=_undefined, c=_undefined)
if a is _undefined: a='test'
if b is _undefined: b=100
if c is _undefined: c=[]
但是,我们已经失去了内省的能力,并且看到了默认参数 是。因为没有构造对象,所以我们不能在没有实际调用函数的情况下抓住它们。我们能做的最好的事情是存储源代码并将其作为字符串返回。
50
2017-07-16 10:05
防御Python的5分
简单:从以下意义上说,行为很简单:
大多数人只陷入这个陷阱一次,而不是几次。
一致性:Python 总是 传递对象,而不是名称。
显然,默认参数是函数的一部分
标题(不是函数体)。因此应该对其进行评估
在模块加载时(并且仅在模块加载时,除非嵌套),不是
在函数调用时。
用处:正如弗雷德里克伦德在他的解释中指出的那样
的 “Python中的默认参数值”,
当前行为对于高级编程非常有用。
(谨慎使用。)
足够的文档:在最基本的Python文档中,
该教程,该问题被大声宣布为
一个 “重要警告” 在里面 第一 章节
“更多关于定义功能”。
警告甚至使用粗体,
很少在标题之外应用。
RTFM:阅读精细手册。
元学习:落入陷阱实际上非常
有用的时刻(至少如果你是一个反思性的学习者),
因为你随后会更好地理解这一点
上面的“一致性”和那将
教你很多关于Python的知识。
47
2018-03-30 11:18
你为什么不反省?
我 真 惊讶没有人进行过Python提供的富有洞察力的内省(2
和 3
适用于)callables。
给出一个简单的小功能 func
定义为:
>>> def func(a = []):
... a.append(5)
当Python遇到它时,它要做的第一件事是编译它以创建一个 code
这个功能的对象。这个编译步骤完成后, 蟒蛇 评估板* 接着 商店 默认参数(空列表 []
这里)在函数对象本身。正如最佳回答所述:列表 a
现在可以被视为一个 会员 功能 func
。
所以,让我们做一些内省,一个前后检查列表如何扩展 内 功能对象。我在用着 Python 3.x
为此,对于Python 2同样适用(使用 __defaults__
要么 func_defaults
在Python 2中;是的,同一件事的两个名字)。
执行前的功能:
>>> def func(a = []):
... a.append(5)
...
在Python执行此定义后,它将采用指定的任何默认参数(a = []
在这里)和 把它们塞进去 __defaults__
函数对象的属性 (相关部分:Callables):
>>> func.__defaults__
([],)
哦,所以一个空列表作为单个条目 __defaults__
,正如预期的那样。
执行后的功能:
现在让我们执行这个函数:
>>> func()
现在,让我们看看那些 __defaults__
再次:
>>> func.__defaults__
([5],)
惊讶? 对象内部的值发生了变化!现在,对函数的连续调用将简单地附加到嵌入式函数 list
目的:
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
所以,你有它,这就是为什么 '缺陷' 发生,是因为默认参数是函数对象的一部分。这里没有什么奇怪的事情,这一切都有点令人惊讶。
解决这个问题的常见解决方案是通常的 None
作为默认值然后在函数体中初始化:
def func(a = None):
# or: a = [] if a is None else a
if a is None:
a = []
由于函数体每次都重新执行,如果没有传递参数,你总是得到一个全新的空列表 a
。
进一步验证列表中的 __defaults__
与函数中使用的相同 func
你可以改变你的功能来返回 id
的清单 a
在函数体内部使用。然后,将其与列表中的列表进行比较 __defaults__
(位置 [0]
在 __defaults__
)你会看到这些确实是如何引用相同的列表实例:
>>> def func(a = []):
... a.append(5)
... return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True
一切都具有内省的力量!
* 要验证Python在编译函数期间评估默认参数,请尝试执行以下操作:
def bar(a=input('Did you just see me without calling the function?')):
pass # use raw_input in Py2
你会注意到的, input()
在构建函数并将其绑定到名称之前调用 bar
是。
43
2017-12-09 07:13