“最小惊讶”与可变默认论证

任何修补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()

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

编辑

Baczek举了个有趣的例子。连同你的大多数评论和尤其是尤塔尔,我进一步阐述了:

>>> def a():...     print("a executed")...     return []...>>>>>> def b(x=a()):...     x.append(5)...     print(x)...a executed>>> b()[5]>>> b()[5, 5]

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

在函数内部进行绑定将意味着x在函数被调用时有效地绑定到指定的默认值,而不是定义,这将呈现一个严重的缺陷:def行将是“混合”,因为部分绑定(函数对象)将在定义时发生,部分(默认参数的分配)在函数调用时发生。

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

235060 次浏览

假设您有以下代码

fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):...

当我看到吃的声明时,最不令人惊讶的是认为如果没有给出第一个参数,它将等于元组("apples", "bananas", "loganberries")

但是,假设在代码的后面,我做了这样的事情

def some_random_function():global fruitsfruits = ("blueberries", "mangos")

然后,如果默认参数是在函数执行而不是函数声明时绑定的,我会惊讶地(以一种非常糟糕的方式)发现水果已经被改变了。在我看来,这比发现你上面的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?

现在,我的map是在StringBuffer键被放入map时使用它的值,还是通过引用来存储键?无论哪种方式,都有人感到惊讶;要么是试图使用与他们放入的值相同的值将对象从Map中取出的人,要么是似乎无法检索他们的对象的人,即使他们使用的键与用于将其放入map的对象实际上是相同的对象(这实际上就是Python不允许将其可变的内置数据类型用作字典键的原因)。

你的例子就是一个很好的例子,说明了Python新手会感到惊讶和被咬伤。但我认为,如果我们“修复”了这个问题,那么只会造成一种不同的情况,让他们反而被咬伤,这种情况会更不直观。此外,在处理可变变量时总是这样;你总是会遇到这样的情况:某人可以根据他们编写的代码直观地期望一种或相反的行为。

我个人喜欢Python当前的方法:默认函数参数在定义函数时被评估,并且该对象始终是默认值。我想他们可以使用空列表进行特殊情况,但这种特殊大小写会引起更多的惊讶,更不用说向后不兼容了。

嗯,原因很简单,绑定是在执行代码时完成的,函数定义是在定义函数时执行的。

比较一下这个:

class BananaBunch:bananas = []
def addBanana(self, banana):self.bananas.append(banana)

此代码遭受完全相同的意外事件。bananas是一个类属性,因此,当您向其添加内容时,它会添加到该类的所有实例。原因完全相同。

这只是“它是如何工作的”,在函数情况下使它以不同的方式工作可能会很复杂,在类情况下可能是不可能的,或者至少会减慢对象实例化的速度,因为你必须保留类代码并在创建对象时执行它。

是的,这是出乎意料的。但是一旦一分钱下降,它就完全符合Python的一般工作方式。事实上,这是一个很好的教具,一旦你理解了为什么会发生这种情况,你就会更好地掌握python。

也就是说,它应该在任何好的Python教程中占据突出位置。因为正如你提到的,每个人迟早都会遇到这个问题。

这种行为很容易解释为:

  1. 函数(类等)声明只执行一次,创建所有默认值对象
  2. 一切都通过引用传递

所以:

def x(a=0, b=[], c=[], d=0):a = a + 1b = b + [1]c.append(1)print a, b, c
  1. a不会改变-每个赋值调用都会创建新的int对象-打印新对象
  2. b不会改变-新数组从默认值构建并打印
  3. c更改-对同一对象执行操作-并打印出来

你要问的是为什么:

def func(a=[], b = 2):pass

在内部并不等同于:

def func(a=None, b = None):a_default = lambda: []b_default = lambda: 2def actual_func(a=None, b=None):if a is None: a = a_default()if b is None: b = b_default()return actual_funcfunc = func()

除了显式调用func(无,无)的情况,我们将忽略它。

换句话说,与其评估默认参数,为什么不存储它们中的每一个,并在调用函数时评估它们?

一个答案可能就在那里——它会有效地将每个带有默认参数的函数变成一个闭包。即使它都隐藏在解释器中,而不是一个完整的闭包,数据也必须存储在某个地方。它会更慢,占用更多内存。

这是一个性能优化。由于这个功能,你认为这两个函数调用中哪个更快?

def print_tuple(some_tuple=(1,2,3)):print some_tuple
print_tuple()        #1print_tuple((1,2,3)) #2

我会给你一个提示。这是反汇编(见http://docs.python.org/library/dis.html):

#1

0 LOAD_GLOBAL              0 (print_tuple)3 CALL_FUNCTION            06 POP_TOP7 LOAD_CONST               0 (None)10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)3 LOAD_CONST               4 ((1, 2, 3))6 CALL_FUNCTION            19 POP_TOP10 LOAD_CONST               0 (None)13 RETURN_VALUE

我怀疑经验丰富的行为是否有实际用途(谁真的在C中使用了静态变量,而没有滋生错误?)

如你所见,使用不可变的默认参数有的性能优势。如果它是一个经常调用的函数,或者默认参数需要很长时间来构造,这可能会有所不同。此外,请记住,Python不是C。在C中,你有非常自由的常量。在Python中,你没有这个好处。

我对Python解释器的内部工作一无所知(我也不是编译器和解释器的专家),所以如果我提出任何不明智或不可能的建议,请不要责怪我。

假设python对象是可变的,我认为在设计默认参数时应该考虑到这一点。当您实例化列表时:

a = []

您希望得到a引用的新的列表。

为什么a=[]

def x(a=[]):

在函数定义而不是调用时实例化一个新列表?这就像你在问“如果用户不提供参数,那么实例化一个新列表,并将其用作调用者生成的列表”。我认为这是模棱两可的:

def x(a=datetime.datetime.now()):

user,您是否希望a默认为您定义或执行x时对应的日期时间?在这种情况下,与前一个一样,我将保持相同的行为,就好像默认参数“赋值”是函数的第一条指令(datetime.now()在函数调用时调用)。另一方面,如果用户想要定义时间映射,他可以这样写:

b = datetime.datetime.now()def x(a=b):

我知道,我知道:这是一个闭包。或者Python可能会提供一个关键字来强制定义时绑定:

def x(static a=b):

我曾经认为在运行时创建对象是更好的方法。现在我不太确定了,因为你确实失去了一些有用的特性,尽管为了防止新手混淆,这可能是值得的。这样做的缺点是:

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: ifuncs = [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=100if c is _undefined: c=[]

然而,我们已经失去了自省的能力,看看默认参数是什么。因为对象还没有被构造出来,我们不能在不实际调用函数的情况下获得它们。我们能做的最好的就是存储源代码并将其作为字符串返回。

最简短的答案可能是“定义就是执行”,因此整个论点没有严格意义。作为一个更做作的例子,你可以引用这个:

def a(): return []
def b(x=a()):print x

希望这足以表明在def语句的执行时不执行默认参数表达式并不容易或没有意义,或两者兼而有之。

不过,我同意当你尝试使用默认构造函数时,这是一个陷阱。

这可能是真的:

  1. 有人正在使用所有语言/库功能,并且
  2. 改变这里的行为是不明智的,但是

保持上述两个特征是完全一致的,并且仍然提出另一个观点:

  1. 这是一个令人困惑的特性,在Python中是不幸的。

其他的答案,或者至少其中的一些,要么提出第1和第2点,而不是第3点,要么提出第3点,淡化第1和第2点。

在这里中途切换马匹可能会要求重大破坏,并且通过更改Python来直观地处理Stefano的开头片段可能会产生更多问题。而且,一个非常了解Python内部结构的人可能会解释后果的雷区。然而,

现有的行为不是Pythonic的,Python之所以成功,是因为该语言几乎没有在任何地方如此严重地违反最小惊讶原则。这是一个真正的问题,无论将其连根拔起是否明智。这是一个设计缺陷。如果你通过追踪行为来更好地理解这门语言,我可以说C++做到了所有这些甚至更多;你通过导航学到了很多东西,例如,微妙的指针错误。但这不是Pythonic:对Python足够关心并坚持面对这种行为的人是被这门语言吸引的人,因为Python比其他语言少得多惊喜。涉猎者和好奇者成为Pythonistas,当他们惊讶于只需要很少的时间就可以让一些东西发挥作用时——不是因为设计——我的意思是,隐藏的逻辑难题——这违背了程序员的直觉,他们被Python吸引是因为它只是工作

实际上,这不是设计缺陷,也不是因为内部或性能。它只是因为Python中的函数是一流的对象,而不仅仅是一段代码。

一旦你这样想,它就完全有意义了:一个函数是一个根据其定义被评估的对象;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用改变到另一个调用——就像在任何其他对象中一样。

无论如何,effbot(Fredrik Lundh)在Python中的默认参数值中对这种行为的原因有很好的解释。我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。

这实际上与默认值无关,除了当您编写具有可变默认值的函数时,它通常会出现意外行为。

>>> def foo(a):a.append(5)print a
>>> a  = [5]>>> foo(a)[5, 5]>>> foo(a)[5, 5, 5]>>> foo(a)[5, 5, 5, 5]>>> foo(a)[5, 5, 5, 5, 5]

这段代码中没有默认值,但您会遇到完全相同的问题。

问题是foo修改,一个从调用者传入的可变变量,当调用者不期望这样时。如果函数被调用为类似append_5的东西,这样的代码会很好;然后调用者会调用该函数以修改他们传入的值,这种行为是预期的。但是这样的函数不太可能接受默认参数,并且可能不会返回列表(因为调用者已经有了对该列表的引用;它刚刚传入的那个)。

带有默认参数的原始foo不应该修改a,无论它是显式传入还是获得默认值。你的代码应该保留可变参数,除非从上下文/名称/留档中可以清楚地看出这些参数应该被修改。使用可变值作为参数传入作为本地临时参数是一个极其糟糕的主意,无论我们是否在Python中,也无论是否涉及默认参数。

如果您需要在计算过程中破坏性地操作局部临时,并且需要从参数值开始操作,则需要复制。

这里的解决方案是:

  1. 使用None作为默认值(或随机数object),然后打开它以在运行时创建值;或者
  2. 使用lambda作为默认参数,并在try块中调用它以获取默认值(这是lambda抽象的用途)。

第二个选项很好,因为函数的用户可以传入一个可调用的,它可能已经存在(例如type

如果考虑到以下因素,这种行为并不奇怪:

  1. 只读类属性在赋值尝试时的行为,以及
  2. 函数是对象(在接受的答案中解释得很好)。

(1)可能是令人惊讶的原因,因为这种行为在来自其他语言时并不“直观”。

(1)在Python课程教程中描述。在尝试为只读类属性赋值时:

…在最内层范围之外找到的所有变量都是只读(尝试写入这样的变量只会创建一个最内层作用域中的新局部变量,留下相同的命名为外部变量不变)。

回顾最初的例子,考虑上述几点:

def foo(a=[]):a.append(5)return a

这里foo是一个对象,afoo的属性(在foo.func_defs[0]处可用)。由于a是一个列表,因此a是可变的,因此是foo的读写属性。它在函数实例化时被初始化为签名指定的空列表,只要函数对象存在,它就可用于读写。

在不覆盖默认值的情况下调用foo将使用foo.func_defs中的默认值。在这种情况下,foo.func_defs[0]用于函数对象代码范围内的a。对a的更改更改了foo.func_defs[0],它是foo对象的一部分,并且在foo中的代码执行之间持续存在。

现在,将其与模拟其他语言的默认参数行为上的留档示例进行比较,以便每次执行函数时都使用函数签名默认值:

def foo(a, L=None):if L is None:L = []L.append(a)return L

考虑到(1)(2),可以看出为什么这会实现所需的行为:

  • foo函数对象被实例化时,foo.func_defs[0]被设置为None,一个不可变的对象。
  • 当函数以默认值执行时(在函数调用中没有为L指定参数),foo.func_defs[0]None)在本地范围内作为L可用。
  • L = []时,赋值不能在foo.func_defs[0]成功,因为该属性是只读的。
  • 每个(1)在局部作用域中创建了一个名为#0的新局部变量并用于函数调用的其余部分。foo.func_defs[0]因此对于foo的未来调用保持不变。

留档的相关部分:

执行函数定义时,默认参数值从左到右求值。这意味着表达式在定义函数时被求值一次,并且每次调用都使用相同的“预计算”值。当默认参数是可变对象(例如列表或字典)时,理解这一点尤为重要:如果函数修改了对象(例如通过将项目附加到列表中),则默认值实际上被修改了。这通常不是预期的。解决此问题的方法是使用None作为默认值,并在函数体中显式地测试它,例如:

def whats_on_the_telly(penguin=None):if penguin is None:penguin = []penguin.append("property of the zoo")return penguin

1)所谓的“可变默认参数”问题通常是一个特殊的例子,证明:
"所有有此问题的函数在实际参数上也有类似的副作用问题,"
这违反了函数式编程的规则,通常是不可取的,应该将两者固定在一起。

示例:

def foo(a=[]):                 # the same problematic functiona.append(5)return a
>>> somevar = [1, 2]           # an example without a default parameter>>> foo(somevar)[1, 2, 5]>>> somevar[1, 2, 5]                      # usually expected [1, 2]

解决方案: a复制
一个绝对安全的解决方案是先#0#1输入对象,然后对副本做任何事情。

def foo(a=[]):a = a[:]     # a copya.append(5)return a     # or everything safe by one line: "return a + [5]"

许多内置的可变类型都有像some_dict.copy()some_set.copy()这样的复制方法,或者像somelist[:]list(some_list)这样容易复制。每个对象也可以被copy.copy(any_object)复制,或者被copy.deepcopy()复制得更彻底(如果可变对象是由可变对象组成的,后者很有用)。有些对象基本上是基于像“文件”对象这样的副作用,不能通过复制有意义地复制。复制

类似的问题的示例问题

class Test(object):            # the original problematic classdef __init__(self, var1=[]):self._var1 = var1
somevar = [1, 2]               # an example without a default parametert1 = Test(somevar)t2 = Test(somevar)t1._var1.append([1])print somevar                  # [1, 2, [1]] but usually expected [1, 2]print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

它不应该保存在此函数返回的实例的任何公共属性中。(假设实例的私人属性不应该根据惯例从该类或子类之外修改。即_var1是私有属性)

结论:
输入参数对象不应该被修改(突变),也不应该被绑定到函数返回的对象中。(如果我们喜欢没有副作用的编程,强烈推荐。参见Wiki关于“副作用”(前两段在这方面是相关的。).)

2)
只有当对实际参数的副作用是必需的,但对默认参数是不需要的,那么有用的解决方案是def ...(var1=None):if var1 is None:var1 = []更多…

3)在某些情况下是默认参数的可变行为有用

您可以通过替换对象(以及与范围的关系)来解决这个问题:

def foo(a=[]):a = list(a)a.append(5)return a

丑陋,但它工作。

使用无的简单解决方法

>>> def bar(b, data=None):...     data = data or []...     data.append(b)...     return data...>>> bar(3)[3]>>> bar(3)[3]>>> bar(3)[3]>>> bar(3, [34])[34, 3]>>> bar(3, [34])[34, 3]

这个“bug”给了我很多加班时间!但我开始看到它的潜在用途(但我仍然希望它在执行时间)

我会给你一个我认为有用的例子。

def example(errors=[]):# statements# Something went wrongmistake = Trueif mistake:tryToFixIt(errors)# Didn't work.. let's try againtryToFixItAnotherway(errors)# This time it workedreturn errors
def tryToFixIt(err):err.append('Attempt to fix it')
def tryToFixItAnotherway(err):err.append('Attempt to fix it by another way')
def main():for item in range(2):errors = example()print '\n'.join(errors)
main()

打印以下

Attempt to fix itAttempt to fix it by another wayAttempt to fix itAttempt to fix it by another way

我认为这个问题的答案在于python如何将数据传递给参数(值传递或引用),而不是可变性或python如何处理“def”语句。

功能概述:首先,python中有两种数据类型,一种是简单基本数据类型,类似于数字,另一种数据类型是对象。其次,在向参数传递数据时,python按值传递基本数据类型,即将值的本地副本复制到局部变量,但通过引用传递对象,即指向对象的指针。

承认以上两点,让我们解释一下python代码发生了什么。这只是因为通过引用传递对象,但与可变/不可变无关,或者可以说“def”语句在定义时只执行一次。

[]是一个对象,所以python将[]的引用传递给a,即a只是一个指向[]的指针,它作为一个对象位于内存中。[]只有一个副本,然而,对它的引用很多。对于第一个foo(),list[]通过append方法更改为1。但请注意,列表对象只有一个副本,这个对象现在变成了1。运行第二个foo()时,effbot网页所说的(不再评估项目)是错误的。a被评估为列表对象,尽管现在对象的内容是1。这就是通过引用传递的效果!foo(3)的结果可以很容易地以同样的方式导出。

为了进一步验证我的答案,让我们看一下另外两个代码。

====== 2 ========

def foo(x, items=None):if items is None:items = []items.append(x)return items
foo(1)  #return [1]foo(2)  #return [2]foo(3)  #return [3]

[]是一个对象,None也是一个对象(前者是可变的,而后者是不可变的。但可变性与问题无关)。无在空间中的某个地方,但我们知道它在那里,而且那里只有一个无的副本。所以每次调用foo时,项目都会被评估(而不是一些回答只评估一次)为无,明确地说,无的引用(或地址)。然后在foo中,项目被更改为[],即指向另一个地址不同的对象。

====== 3 =======

def foo(x, items=[]):items.append(x)return items
foo(1)    # returns [1]foo(2,[]) # returns [2]foo(3)    # returns [1,3]

调用foo(1)使项目指向一个地址为11111111的列表对象[]。在续集的foo函数中,列表的内容更改为1,但地址没有更改,仍然是11111111。然后foo(2,[])来了。虽然foo(2,[])中的[]在调用foo(1)时与默认参数[]的内容相同,但它们的地址不同!由于我们显式提供了参数,items必须获取这个新[]的地址,例如2222222,并在进行一些更改后返回。现在执行foo(3)。由于只提供了x,项目必须再次采用其默认值。默认值是多少?它是在定义foo函数时设置的:位于11111111的列表对象。因此,项目被评估为具有元素1的地址11111111。位于2222222的列表也包含一个元素2,但它不再被项目指向。因此,3的追加将使items[1,3]。

从上面的解释可以看出,已接受答案中推荐的effbot网页未能给出这个问题的相关答案。而且,我认为effbot网页中的一个点是错误的。我认为关于UI. Button的代码是正确的:

for i in range(10):def callback():print "clicked button", iUI.Button("button %s" % i, callback)

每个按钮都可以保存一个不同的回调函数,它将显示不同的i值。我可以提供一个示例来展示这一点:

x=[]for i in range(10):def callback():print(i)x.append(callback)

如果我们执行x[7](),我们将得到7,x[9]()将得到9,i的另一个值。

当我们这样做时:

def foo(a=[]):...

…如果调用者没有传递a的值,我们将参数a分配给无名列表。

为了使讨论更简单,让我们暂时给未命名列表一个名称。pavlo怎么样?

def foo(a=pavlo):...

在任何时候,如果调用者没有告诉我们a是什么,我们都会重用pavlo

如果pavlo是可变的(可修改的),而foo最终修改了它,我们会在下次调用foo时注意到一个效果,而不指定a

这就是你所看到的(记住,pavlo初始化为[]):

 >>> foo()[5]

现在,pavlo是[5]。

再次调用foo()再次修改pavlo

>>> foo()[5, 5]

调用foo()时指定a可确保不触及pavlo

>>> ivan = [1, 2, 3, 4]>>> foo(a=ivan)[1, 2, 3, 4, 5]>>> ivan[1, 2, 3, 4, 5]

所以,pavlo仍然是[5, 5]

>>> foo()[5, 5, 5]

我有时会利用此行为作为以下模式的替代方案:

singleton = None
def use_singleton():global singleton
if singleton is None:singleton = _make_singleton()
return singleton.use_me()

如果singleton仅被use_singleton使用,我喜欢以下模式作为替换:

# _make_singleton() is called only once when the def is executeddef use_singleton(singleton=_make_singleton()):return singleton.use_me()

我使用它来实例化访问外部资源的客户端类,以及创建用于记忆的文本或列表。

由于我不认为这种模式是众所周知的,我做了一个简短的评论,以防止未来的误解。

已经很忙的话题,但从我在这里读到的,以下内容帮助我意识到它是如何在内部工作的:

def bar(a=[]):print id(a)a = a + [1]print id(a)return a
>>> bar()44843702324484524224[1]>>> bar()44843702324484524152[1]>>> bar()4484370232 # Never change, this is 'class property' of the function4484523720 # Always a new object[1]>>> id(bar.func_defaults[0])4484370232

5分防御Python

  1. 简单:行为在以下意义上很简单:大多数人只掉进这个陷阱一次,而不是几次。

  2. 一致性:Python总是传递对象,而不是名称。默认参数显然是函数的一部分标题(而不是函数体)。因此应该对其进行评估在模块加载时(并且仅在模块加载时,除非嵌套),而不是在函数调用时。

  3. 有用性:正如弗雷德里克·伦德在他的解释中指出的“Python中的默认参数值”的当前行为对于高级编程非常有用。(小心使用)

  4. 留档充足:在最基本的Python留档中,教程,问题被大声宣布为“重要警告”在第第一小节中“更多关于定义函数”警告甚至使用黑体字,它很少应用于标题之外。RTFM:阅读精美手册。

  5. 元学习:落入陷阱实际上是一个非常有用的时刻(至少如果你是一个反思型学习者)因为你随后会更好地理解这一点上面的“一致性”,这将教你很多关于Python的知识。

只需将函数更改为:

def notastonishinganymore(a = []):'''The name is just a joke :)'''a = a[:]a.append(5)return a

我将演示另一种将默认列表值传递给函数的结构(它与字典同样有效)。

正如其他人广泛评论的那样,list参数在定义时而不是在执行时绑定到函数。因为列表和字典是可变的,对该参数的任何更改都将影响对该函数的其他调用。结果,对该函数的后续调用将收到这个共享列表,该列表可能已被对该函数的任何其他调用更改。更糟糕的是,两个参数同时使用该函数的共享参数,而不会注意另一个参数所做的更改。

错误的方法(可能…)

def foo(list_arg=[5]):return list_arg
a = foo()a.append(6)>>> a[5, 6]
b = foo()b.append(7)# The value of 6 appended to variable 'a' is now part of the list held by 'b'.>>> b[5, 6, 7]
# Although 'a' is expecting to receive 6 (the last element it appended to the list),# it actually receives the last element appended to the shared list.# It thus receives the value 7 previously appended by 'b'.>>> a.pop()7

您可以使用id验证它们是同一个对象:

>>> id(a)5347866528
>>> id(b)5347866528

根据Brett Slatkin的“有效的Python:编写更好的Python的59种具体方法”,第20项:使用#0和Docstring指定动态默认参数(第48页)

在Python中实现所需结果的约定是提供默认值None并记录实际行为在docstring中。

此实现确保对函数的每次调用要么接收默认列表,要么接收传递给函数的列表。

首选方法

def foo(list_arg=None):""":param list_arg:  A list of input values.If none provided, used a list with a default value of 5."""if not list_arg:list_arg = [5]return list_arg
a = foo()a.append(6)>>> a[5, 6]
b = foo()b.append(7)>>> b[5, 7]
c = foo([10])c.append(11)>>> c[10, 11]

“错误方法”可能有合法的用例,程序员希望共享默认列表参数,但这更可能是例外而不是规则。

你为什么不反省一下?

我很惊讶没有人在可调用对象上执行Python提供的富有洞察力的内省(23适用)。

给定一个简单的小函数func定义为:

>>> def func(a = []):...    a.append(5)

当Python遇到它时,它会做的第一件事就是编译它,以便为这个函数创建一个code对象。当这个编译步骤完成时,Python计算*,然后将默认参数(此处为空列表#1)存储在函数对象本身中。正如上面的答案所提到的:列表a现在可以被视为函数func成员

所以,让我们做一些自省,之前和之后来检查列表是如何扩展里面函数对象的。为此我使用Python 3.x,对于Python 2来说也是如此(在Python 2中使用__defaults__func_defaults;是的,同样的事情有两个名字)。

执行前的功能:

>>> def func(a = []):...     a.append(5)...

在Python执行此定义后,它将采用指定的任何默认参数(此处为a = [])和将它们塞进函数对象的#1属性中(相关部分:Callables):

>>> func.__defaults__([],)

O. k,所以一个空列表作为__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 aif a is None:a = []

由于每次都重新执行函数体,如果没有为a传递参数,您总是会得到一个新的空列表。


为了进一步验证__defaults__中的列表与函数func中使用的列表相同,您可以更改您的函数以返回函数体内使用的列表a中的id。然后,将其与__defaults__中的列表进行比较(__defaults__中的位置[0]),您将看到这些确实引用了相同的列表实例:

>>> 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

您会注意到,在构建函数并将其绑定到名称bar之前调用了input()

Python:可变默认参数

默认参数在函数编译为函数对象时被评估。当函数多次使用时,它们是并保持相同的对象。

当它们是可变的时,当突变时(例如,通过向其添加元素),它们在连续调用时保持突变。

它们保持突变,因为它们每次都是相同的对象。

等效代码:

由于在编译和实例化函数对象时列表绑定到函数,因此:

def foo(mutable_default_argument=[]): # make a list the default argument"""function that uses a list"""

几乎完全等同于这个:

_a_list = [] # create a list in the globals
def foo(mutable_default_argument=_a_list): # make it the default argument"""function that uses a list"""
del _a_list # remove globals name binding

演示

这是一个演示-您可以验证它们每次被引用时都是同一个对象

  • 看到列表是在函数完成编译为函数对象之前创建的,
  • 注意到每次引用列表时id都是相同的,
  • 观察到列表在使用它的函数第二次被调用时保持变化,
  • 观察从源(我方便地为您编号)打印输出的顺序:

example.py

print('1. Global scope being evaluated')
def create_list():'''noisily create a list for usage as a kwarg'''l = []print('3. list being created and returned, id: ' + str(id(l)))return l
print('2. example_function about to be compiled to an object')
def example_function(default_kwarg1=create_list()):print('appending "a" in default default_kwarg1')default_kwarg1.append("a")print('list with id: ' + str(id(default_kwarg1)) +' - is now: ' + repr(default_kwarg1))
print('4. example_function compiled: ' + repr(example_function))

if __name__ == '__main__':print('5. calling example_function twice!:')example_function()example_function()

并用python example.py运行它:

1. Global scope being evaluated2. example_function about to be compiled to an object3. list being created and returned, id: 1405027588080324. example_function compiled: <function example_function at 0x7fc9590905f0>5. calling example_function twice!:appending "a" in default default_kwarg1list with id: 140502758808032 - is now: ['a']appending "a" in default default_kwarg1list with id: 140502758808032 - is now: ['a', 'a']

这是否违反了“最小惊讶”原则?

这种执行顺序经常让Python的新用户感到困惑。如果您了解Python执行模型,那么它就会变得非常令人期待。

对新Python用户的常规说明:

但这就是为什么对新用户的通常指令是像这样创建他们的默认参数:

def example_function_2(default_kwarg=None):if default_kwarg is None:default_kwarg = []

这使用无单例作为哨兵对象来告诉函数我们是否得到了默认值以外的参数。如果我们没有得到参数,那么我们实际上想要使用一个新的空列表[]作为默认值。

正如关于控制流的教程部分所说:

如果您不希望在后续调用之间共享默认值,您可以像这样编写函数:

def f(a, L=None):if L is None:L = []L.append(a)return L

这不是设计缺陷.任何被这件事绊倒的人都做错了。

有3种情况下,我看到你可能会遇到这个问题:

  1. 您打算修改参数作为函数的副作用。在这种情况下,永远都说不通具有默认参数。唯一的例外是当您滥用参数列表以具有函数属性时,例如cache={},并且您根本不需要使用实际参数调用函数。
  2. 您打算保持参数不变,但您不小心确实修改了它。这是一个bug,修复它。
  3. 你打算修改参数以在函数内部使用,但不期望修改可以在函数外部查看。在这种情况下,你需要对参数创建复制,无论它是否是默认值!Python不是按值调用语言,所以它不会为你复制,你需要明确说明。

问题中的示例可能属于类别1或3。奇怪的是,它既修改传递的列表又返回它;你应该选择一个或另一个。

TLDR:定义时间默认值是一致的,并且更具表现力。


定义一个函数会影响两个作用域:定义作用域包含是函数,执行作用域载于是函数。虽然块如何映射到作用域很清楚,但问题是def <name>(<args=defaults>):属于哪里:

...                           # defining scopedef name(parameter=default):  # ???...                       # execution scope

def name部分必须在定义范围内进行评估-毕竟我们希望name在那里可用。仅在函数内部评估会使其无法访问。

由于parameter是一个常量名称,我们可以与def name同时“评估”它。这也有一个优点,它产生的函数具有已知的签名name(parameter=...):,而不是裸name(...):

什么时候评估default

一致性已经说过“在定义时”:def <name>(<args=defaults>):的所有其他内容也最好在定义时进行评估。延迟它的一部分将是令人惊讶的选择。

这两个选项也不等价:如果default在定义时求值,它仍然可以会影响执行时间。如果default在执行时求值,它不能会影响定义时间。选择“在定义时”允许表达两种情况,而选择“在执行时”只能表达一种情况:

def name(parameter=defined):  # set default at definition time...
def name(parameter=default):     # delay default until execution timeparameter = default if parameter is None else parameter...

每一个其他的答案都解释了为什么这实际上是一个很好的和期望的行为,或者为什么你无论如何都不应该需要这个。我的答案是给那些固执的人的,他们想行使他们的权利,让语言屈从于他们的意愿,而不是相反。

我们将使用一个装饰器“修复”这种行为,该装饰器将复制默认值,而不是为每个保留在默认值的位置参数重用相同的实例。

import inspectfrom copy import deepcopy  # copy would fail on deep arguments like nested dicts
def sanify(function):def wrapper(*a, **kw):# store the default valuesdefaults = inspect.getargspec(function).defaults # for python2# construct a new argument listnew_args = []for i, arg in enumerate(defaults):# allow passing positional argumentsif i in range(len(a)):new_args.append(a[i])else:# copy the valuenew_args.append(deepcopy(arg))return function(*new_args, **kw)return wrapper

现在让我们使用这个装饰器重新定义我们的函数:

@sanifydef foo(a=[]):a.append(5)return a
foo() # '[5]'foo() # '[5]' -- as desired

这对于接受多个参数的函数特别整洁。比较:

# the 'correct' approachdef bar(a=None, b=None, c=None):if a is None:a = []if b is None:b = []if c is None:c = []# finally do the actual work

# the nasty decorator hack@sanifydef bar(a=[], b=[], c=[]):# wow, works right out of the box!

重要的是要注意,如果您尝试使用关键字args,上述解决方案会中断,如下所示:

foo(a=[4])

可以调整装饰器以允许这样做,但我们将此作为读者的练习;)

有一个简单的方法来理解为什么会发生这种情况。

Python在命名空间中从上到下执行代码。

“内部”正好体现了这一规则。

这样选择的原因是“让语言适合你的头脑”。所有奇怪的角落情况都倾向于简化在命名空间中执行代码:默认不可变项、嵌套函数、类(编译完成后稍加修补)、self参数等。类似地,复杂的语法可以用简单的语法编写:a.foo(...)只是a.lookup('foo').__call__(a,...)。这适用于列表理解;装饰器;元类;等等。这让你对奇怪的角落有了近乎完美的了解。该语言适合你的头脑。

你应该坚持下去。学习Python在语言上有一段时间的抱怨,但它变得很舒服。这是我使用过的唯一一种越看越简单的语言。

继续黑客!保持笔记。

对于您的特定代码,详细说明:

def foo(a=[]):a.append(5)return a
foo()

是一个语句,相当于:

  1. 开始创建一个代码对象。
  2. 现在就解释(a=[])[]是参数a的默认值。它是列表类型,[]总是如此。
  3. :之后的所有代码编译为Python字节码并将其粘贴在另一个列表中。
  4. 使用代码字段中的参数和代码创建可调用字典
  5. 将可调用对象添加到'foo'字段中的当前命名空间。

然后,它转到下一行,foo()

  1. 这不是保留字,所以在命名空间里查一下
  2. 调用该函数,该函数将使用列表作为默认参数。开始在其命名空间中执行其字节码。
  3. append不会创建新列表,因此会修改旧列表。

是的,这是Python中的一个设计缺陷

我读过所有其他的答案,我不相信。这个设计确实违反了最小惊讶原则。

默认值可以设计为在调用函数时评估,而不是在定义函数时评估。Javascript是这样做的:

function foo(a=[]) {a.push(5);return a;}console.log(foo()); // [5]console.log(foo()); // [5]console.log(foo()); // [5]

作为这是一个设计缺陷的进一步证据,Python核心开发人员目前正在讨论引入新的语法来解决这个问题。请参阅本文:Python的后期绑定参数默认值

为了获得更多的证据证明这是一个设计缺陷,如果你谷歌“Python陷阱”,这个设计会在前9个谷歌结果(123456789)中被提到为陷阱,通常是列表中的第一个陷阱。相比之下,如果你谷歌“Javascript陷阱”,Javascript中默认参数的行为甚至一次也没有被提到为陷阱。

根据定义,Gotchas违反了最小惊讶原则。它们令人惊讶。鉴于默认参数值的行为有更好的设计,不可避免的结论是Python在这里的行为代表了一个设计缺陷。