“最少惊讶”和可变默认参数


2593

长时间修改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中使用了静态变量,却没有滋生bug?)

编辑

巴泽克举了一个有趣的例子。连同您的大多数评论,特别是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从绑定的一部分(即函数对象)将在定义时发生,部分(默认参数的分配)将在函数调用时发生。

实际行为更加一致:执行该行时将评估该行的所有内容,即在函数定义时进行评估。



4
我毫不怀疑,可变论点对普通人而言至少违反了令人惊讶的原则,而且我已经看到初学者步入那里,然后英勇地用邮件元组替换了邮件列表。然而,可变参数仍然与Python Zen(Pep 20)保持一致,并且属于“对荷兰语显而易见”(硬核python程序员理解/利用)子句。建议使用doc字符串的变通办法是最好的,但是如今对doc字符串和任何(书面)文档的抵制并不少见。就个人而言,我更喜欢装饰器(例如@fixed_defaults)。
Serge

5
当我遇到这个问题时,我的论据是:“为什么您需要创建一个返回可变变量的函数,该可变变量可以选择是可变变量,您将传递给该函数?要么更改一个可变变量,要么创建一个新的可变变量。为什么需要同时使用一个函数来实现这两个功能?为什么要重写解释器以允许您在不向代码中添加三行的情况下做到这一点?” 因为我们在这里谈论的是重写器解释器处理函数定义和调用的方式。对于几乎没有必要的用例,这可以做很多事情。
艾伦·洛特哈德

12
“ Python新手希望此函数始终返回仅包含一个元素的列表:” [5]。我是Python的新手,我不会期望这样做,因为显然foo([1])会返回[1, 5],而不是[5]。您的意思是说,新手希望没有参数的函数将始终返回[5]
symplectomorphic

2
这个问题问“为什么这种(​​错误的方式)如此实现?” 它不会问“正确的方法是什么?” ,其内容涵盖了[ 为什么使用arg = None可以解决Python的可变默认参数问题?] *(stackoverflow.com/questions/10676729 / ...)。新用户几乎总是对前者不那么感兴趣,而对后者更感兴趣,因此有时这是一个非常有用的链接/重复引用。
smci

Answers:


1612

实际上,这不是设计缺陷,也不是由于内部因素或性能所致。
这完全是因为Python中的函数是一流的对象,而不仅仅是一段代码。

一旦您想到这种方式,就完全有道理了:函数是根据其定义求值的对象;默认参数属于“成员数据”,因此它们的状态可能会从一个调用更改为另一个调用-完全与其他任何对象一样。

无论如何,Effbot 在Python的Default Parameter Values中都很好地解释了这种现象的原因。
我发现它很清晰,我真的建议您阅读它,以更好地了解函数对象的工作原理。


80
对于阅读以上答案的任何人,我强烈建议您花时间阅读链接的Effbot文章。以及所有其他有用的信息,关于如何使用此语言功能进行结果缓存/存储的部分非常容易知道!
杰克逊

85
即使它是一流的对象,也可能会设想一种设计,其中将每个默认值的代码与对象一起存储,并在每次调用函数时重新评估。我并不是说这样做会更好,只是功能不是一流的对象并不能完全排除它。
Gerrit

312
抱歉,但是任何被认为是“ Python中最大的WTF”的东西绝对是设计缺陷。在某些时候,这是每个人的错误源,因为没人会一开始就期望这种行为-这意味着它不应该以这种方式设计。我不在乎他们必须克服什么障碍,他们应该设计Python,以便默认参数是非静态的。
BlueRaja-Danny Pflughoeft13年

192
不管它是否是设计缺陷,您的回答似乎都暗示这种行为在某种程度上是必要的,自然的并且显而易见的,因为函数是一流的对象,而事实并非如此。Python有闭包。如果将默认参数替换为函数第一行的赋值,它将对每个调用的表达式求值(可能使用在范围内声明的名称)。完全没有理由说,每次以完全相同的方式调用该函数时,对默认参数进行求值是不可能或不合理的。
Mark Amery 2014年

24
设计并非直接来自functions are objects。在您的范例中,建议是将函数的默认值实现为属性而不是属性。
2014年

273

假设您有以下代码

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

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

但是,假设稍后在代码中,我做类似

def some_random_function():
    global fruits
    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) );  // does this work?

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

您的示例很好地说明了Python新手会感到惊讶和被咬的情况。但是我认为,如果我们“解决”这个问题,那只会造成一种不同的情况,那就是被它们咬住,而且这种情况甚至不那么直观。而且,在处理可变变量时总是如此。您总是遇到这样的情况:根据编写的代码,某人可以直观地预期一种或相反的行为。

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


30
我认为这是一个辩论的问题。您正在作用于全局变量。现在,在代码中任何涉及全局变量的地方执行的任何评估都将(正确)引用为(“蓝莓”,“芒果”)。默认参数可能与其他任何情况一样。
Stefano Borini,2009年

47
实际上,我认为我不同意您的第一个例子。我不确定我一开始喜欢像这样修改初始化程序的想法,但是如果我这样做了,我希望它的行为与您描述的完全相同-将默认值更改为("blueberries", "mangos")
本·布兰克

12
默认参数像任何其他情况。出乎意料的是,该参数是全局变量,而不是局部变量。这又是因为代码是在函数定义而不是调用处执行的。一旦了解了这一点,对于类也是如此,这是非常清楚的。
Lennart Regebro

17
我发现该示例具有误导性,而不是辉煌。如果some_random_function()追加fruits而不是分配给它,的行为eat() 改变。对于当前的出色设计而言,已经足够了。如果您使用在其他地方引用的默认参数,然后从函数外部修改引用,那么您将遇到麻烦。真正的WTF是当人们定义了一个新的默认参数(列表文字或对构造函数的调用)时,仍然可以得到位。
亚历克西斯2014年

13
您只是显式声明global并重新分配了元组-如果eat此后的工作方式不同,绝对没有什么奇怪的。
user3467349

241

文档的相关部分:

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

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

180
短语“这通常不是预期的结果”和“解决此问题的方法”闻起来像是他们在记录设计缺陷。
2014年

4
@Matthew:我很清楚,但这不值得一试。通常,由于这个原因,您通常会看到样式指南和linters无条件地将可变的默认值标记为错误。做同样事情的显式方法是将属性填充到函数(function.data = [])上,或者更好地创建一个对象。
2014年

6
@bukzor:陷阱需要记录并记录下来,这就是为什么这个问题很好并且得到了很多好评的原因。同时,陷阱并不一定要消除。有多少Python初学者将列表传递给修改它的函数,并震惊地看到更改显示在原始变量中?但是,当您了解如何使用可变对象类型时,它们是很棒的。我想这只能归结为对这个特殊陷阱的看法。
马修

33
“这不是通常的意图”一词的意思是“不是程序员实际上想发生的事情”,而不是“不是Python应该做的事情”。
holdenweb 2014年

4
@holdenweb哇,我晚会很晚。在上下文的情况下,bukzor是完全正确的:当他们决定语言应执行函数的定义时,他们在记录并非“预期”的行为/后果。由于这是他们设计选择的意外结果,所以这是设计缺陷。如果这不是设计缺陷,那么甚至不需要提供“解决方案”。
code_dredd

118

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

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

a = []

您希望获得由引用的列表a

为什么要a=[]

def x(a=[]):

在函数定义而不是调用上实例化一个新列表?就像您要问“如果用户不提供参数,则实例化一个新列表并像调用方产生的那样使用它”。我认为这是模棱两可的:

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

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

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

我知道,我知道:那是一个封闭。另外,Python可以提供一个关键字来强制定义时间绑定:

def x(static a=b):

11
您可以这样做:def x(a = None):然后,如果a为None,则设置a = datetime.datetime.now()
Anon

20
这次真是万分感谢。我无法真正地指出为什么这让我无休止。您已经做得很漂亮,并且没有太多的绒毛和混乱。当有人使用C ++进行系统编程时,有时是天真的“翻译”语言功能时,这个虚假的朋友在大笔交易中就把我踢了,就像类属性一样。我理解为什么事情会这样,但不管它带来什么积极的影响,我都忍不住喜欢。至少这与我的经验背道而驰,以至于我可能(希望)永远不会忘记……
AndreasT 2011年

5
@Andreas一旦您使用Python足够长的时间,您就会开始了解Python用它的方式将事物解释为类属性的逻辑性-这仅是因为C ++(和Java,以及C#...)认为将class {}块的内容解释为属于实例是有意义的:)但是,当类是一类对象时,显然很自然的是它们的内容(在内存中)反映了它们的内容(以代码形式)。
Karl Knechtel

6
在我的书中,规范结构不是怪癖或限制。我知道它可能笨拙且丑陋,但您可以将其称为某些事物的“定义”。动态语言在我看来有点像无政府主义者:确保每个人都是自由的,但是您需要结构来让某人清空垃圾并铺平道路。猜猜我老了... :)
AndreasT 2011年

4
功能定义在模块加载时执行。函数在函数调用时执行。默认参数是函数定义的一部分,而不是函数主体。(对于嵌套函数,它变得更加复杂。)
Lutz Prechelt 2015年

84

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

比较一下:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

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

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

是的,这是意外的。但是一旦一分钱下降,它就完全适合Python的工作方式。实际上,这是一个很好的教学辅助工具,一旦您了解了为什么会发生这种情况,就可以更好地使用python。

也就是说,它应该在任何优秀的Python教程中都非常突出。因为正如您提到的,每个人迟早都会遇到此问题。


如何定义每个类实例不同的类属性?
Kieveli

19
如果每个实例都不相同,则不是类属性。类属性是CLASS上的属性。由此得名。因此,它们对于所有实例都是相同的。
Lennart Regebro

1
您如何在类中定义对于类的每个实例都不同的属性?(为那些无法确定不熟悉Python命名约定的人可能正在询问类的普通成员变量的人重新定义)。
Kieveli

@Kievieli:您正在谈论的是类的普通成员变量。:-)您可以通过任何方法说出self.attribute = value来定义实例属性。例如__init __()。
Lennart Regebro

@Kieveli:两个答案:不能,因为您在类级别定义的任何东西都将是一个类属性,而访问该属性的任何实例都将访问相同的类属性。您可以使用propertys / sort of /,它们实际上是类级别的函数,其功能类似于普通属性,但将属性保存在实例中而不是类中(self.attribute = value如Lennart所说)。
伊桑·弗曼

66

你为什么不自省?

我真的惊讶,没有人对可调用对象执行Python提供的深刻的自省(23适用)。

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

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

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

因此,让我们进行一些自省,前后检查清单如何在内部扩展在函数对象。我Python 3.x为此使用,对于Python 2同样适用(在python 2中使用__defaults__func_defaults;是的,同一事物有两个名称)。

执行前的功能:

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


要进一步验证in中的列表__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


1
id(...)最后验证是否需要,或者is操作员会回答相同的问题?
das-g

1
@ das-g is会很好,我刚刚使用过,id(val)因为我认为它可能更直观。
Dimitris Fasarakis Hilliard

将其None用作默认值会严重限制__defaults__自省的用途,因此,我认为这不能很好地捍卫自省__defaults__方式。惰性求值可以使函数默认值对双方都有用。
Brilliand

58

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

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=[]

但是,我们失去了自省的能力,无法看到默认参数。由于尚未构造对象,因此,如果不实际调用函数,就无法拥有它们。我们最好的办法是存储源代码,并将其作为字符串返回。


1
如果每个函数都有一个创建默认参数而不是值的函数,则也可以实现自省。检查模块将仅调用该函数。
yairchu

@SilentGhost:我正在谈论是否已更改行为以重新创建它-当前行为是创建一次,以及为什么存在可变的默认问题。
布赖恩

1
@yairchu:假设这样做是安全的(即没有副作用)。对args进行内省不应该任何事情,但是评估任意代码很可能会产生效果。
布赖恩

1
不同的语言设计通常只是意味着编写不同的东西。您的第一个示例可以轻松地写为:def foo(arg = _expensive),如果您希望重新评估它。
格伦·梅纳德

@Glenn-这就是我所说的“从外部缓存变量”的含义-有点冗长,但是最终在名称空间中出现了额外的变量。
布赖恩

55

捍卫Python的5分

  1. 简单性:行为在以下意义上是简单的:大多数人只会陷入一次陷阱,而不是几次。

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

  3. 用途:正如Frederik Lundh在对“ Python中的默认参数值”的解释中所指出的那样,当前行为对于高级编程可能非常有用。(请谨慎使用。)

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

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


18
我花了一年时间才发现这种行为使我的生产代码混乱不堪,最终删除了一个完整的功能,直到我偶然碰到了这个设计缺陷。我正在使用Django。由于暂存环境没有很多请求,因此该错误永远不会对质量检查产生任何影响。当我们上线并收到许多同时请求时-一些实用程序功能开始覆盖彼此的参数!造成安全漏洞,漏洞以及其他漏洞。
oriadam 2015年

7
@oriadam,没有冒犯,但我想知道您是如何在不接触Python的情况下学习Python的。我现在只是在学习Python ,在官方Python教程中提到了这种可能的陷阱同时也第一次提到了默认参数。(如该答案的第4点所述。)我认为道德(而不是同情地)是阅读用于创建生产软件的语言的官方文档
通配符

另外,对我来说,如果除了我正在执行的函数调用之外还调用了复杂性未知的函数,这将令我感到惊讶。
Vatine 2013年

52

此行为很容易通过以下方式解释:

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

所以:

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

(实际上,是一个不好的例子,但整数仍然是不变的是我的主要观点。)
Anon

在检查以确保将b设置为[]的情况下,意识到了这一点,b .__ add __([1])返回[1],即使列表是可变的,b仍然[]。我的错。
佚名

@ANon:有__iadd__,但不适用于int。当然。:-)
Veky

35

您要问的是为什么这样:

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

在内部不等同于此:

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

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

换句话说,为什么不存储默认参数,而不是评估默认参数,并在调用函数时对其进行评估?

一个答案可能就在那里-它可以有效地将具有默认参数的每个函数转换为闭包。即使全部隐藏在解释器中,而不是完全关闭,数据也必须存储在某个地方。它将变慢,并使用更多的内存。


6
不必是闭包-想到它的更好方法是使字节码创建默认代码的第一行-毕竟您要编译该主体之后-代码之间没有真正的区别在正文中的参数和代码中。
布赖恩

10
是的,但这仍然会降低Python的运行速度,这实际上是相当令人惊讶的,除非您对类定义进行相同的操作,这将使其变得非常缓慢,因为您每次实例化a时都必须重新运行整个类定义。类。如前所述,解决方案比问题要令人惊讶。
Lennart Regebro

与Lennart达成协议。正如Guido喜欢说的那样,对于每种语言功能或标准库,都有在使用它。
杰森·贝克

6
现在更改它将是一种精神错乱-我们只是在探索为何如此。如果它从一开始就进行了默认违约评估,那并不一定会令人惊讶。毫无疑问,这样一个核心的解析差异会对整个语言产生巨大的影响,并且可能会产生许多晦涩的影响。
格伦·梅纳德

35

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

例:

def foo(a=[]):                 # the same problematic function
    a.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]

解决方案:一个副本
的绝对安全解决方案是copydeepcopy输入对象进行操作,然后对副本执行任何操作。

def foo(a=[]):
    a = a[:]     # a copy
    a.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()(后者有用如果可变对象是从可变对象构成)。有些对象从根本上是基于副作用的,例如“文件”对象,并且不能通过复制有意义地进行复制。复制中

示例问题 类似的SO问题的

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = 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 = [] More。

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


5
我希望您知道Python 不是一种功能编程语言。
Veky

6
是的,Python是一种具有多种功能的多范式语言。(“不要仅仅因为有锤子就让每个问题看起来都像钉子一样。”)许多问题都是Python的最佳实践。Python有一个有趣的HOWTO函数式编程其他功能是闭包和currying,此处未提及。
hynekcer 2014年

1
在后期,我还要补充一点,Python的赋值语义已明确设计为避免在必要时进行数据复制,因此创建副本(尤其是深层副本)会对运行时和内存使用产生不利影响。因此,仅在必要时才使用它们,但是新手通常很难理解。
holdenweb

1
@holdenweb我同意。临时副本是最常用的方法,有时也是唯一的方法,如何保护原始可变数据免受可能对其进行修改的无关功能的影响。幸运的是,不合理修改数据的功能被认为是错误,因此不常见。
hynekcer

我同意这个答案。而且我不明白def f( a = None )当您真的要表达其他含义时,为什么建议使用此构造。可以复制,因为您不应该对参数进行突变。而且,当您这样做时if a is None: a = [1, 2, 3],还是要复制列表。
koddo

30

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

>>> 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,是否涉及默认参数。

如果您需要在计算内容的过程中破坏性地操作本地临时文件,并且需要从参数值开始进行操作,则需要进行复制。


7
尽管相关,但我认为这是不同的行为(因为我们希望“就地” append更改a)。一个默认的可变未在每次调用重新实例是“意外”有点...至少对我来说。:)
安迪·海登

2
@AndyHayden如果希望函数修改参数,为什么使用默认值有意义?
Mark Ransom

@MarkRansom我唯一能想到的例子是cache={}。但是,我怀疑这种“最少的惊讶”是在您希望(或不希望)您要调用的用于使参数发生变化的函数时发生的。
安迪·海登

1
@AndyHayden我在这里留下了自己的答案,并补充了这种观点。让我知道你的想法。cache={}为了完整性,我可能会将您的示例添加到其中。
Mark Ransom

1
@AndyHayden我的回答的重点是,如果您不小心更改了参数的默认值而感到惊讶,那么您还会遇到另一个错误,那就是您的代码可以在不使用默认值的情况下意外地更改调用者的值。并请注意,None如果arg是,则使用并分配实际的默认值None 不能解决该问题(因此,我认为这是一种反模式)。如果通过避免改变参数值是否具有默认值来解决另一个错误,那么您将永远不会注意到或关心这种“令人惊讶”的行为。

27

话题已经很忙了,但是根据我在这里所读到的内容,以下内容帮助我意识到了它在内部的工作方式:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

2
实际上,这对于新手来说可能有点令人困惑,因为a = a + [1]重载a...考虑将其更改为b = a + [1] ; print id(b)并添加一行a.append(2)。这样可以更明显地看出,+在两个列表上总会创建一个新列表(分配给b),而修改后的列表a仍然可以具有相同的id(a)
约恩·希斯

25

这是一项性能优化。通过此功能,您认为这两个函数调用中哪个更快?

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

print_tuple()        #1
print_tuple((1,2,3)) #2

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

#1个

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

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

正如你所看到的,用一成不变的默认参数时提高性能。如果这是一个经常调用的函数,或者默认参数需要花费很长时间来构造,那么这可能会有所不同。另外,请记住,Python不是C。在C中,您拥有几乎免费的常量。在Python中,您没有此好处。


24

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 evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

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

这种执行顺序经常会使Python的新用户感到困惑。如果您了解Python执行模型,那么就可以预期了。

对新Python用户的一般说明:

但这就是为什么对新用户的通常指示是改为创建其默认参数,如下所示:

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

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

正如关于控制流教程部分所述

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

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

24

最短的答案可能是“定义就是执行”,因此整个论点没有严格意义。作为更人为的示例,您可以引用以下内容:

def a(): return []

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

希望足以表明在def语句执行时不执行默认参数表达式不是一件容易的事,或者说没有道理,或者两者兼而有之。

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


20

使用None的简单解决方法

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

19

如果考虑以下因素,这种行为就不足为奇了:

  1. 分配尝试时只读类属性的行为,并且
  2. 函数是对象(在接受的答案中有很好的解释)。

(2)的作用已在该线程中广泛讨论。(1)可能是令人惊讶的原因,因为这种行为在来自其他语言时不是“直观”的。

(1)有关类的Python 教程中进行了描述。在尝试为只读类属性分配值时:

...在最内层作用域之外找到的所有变量都是只读的(尝试写入此类变量只会在最内层作用域内创建一个新的局部变量,而使名称相同的外层变量保持不变)。

回到原始示例并考虑以上几点:

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

foo是一个对象,a是的属性foo(位于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)还会L在本地范围内创建一个也命名为新的本地变量,并用于其余的函数调用。foo.func_defs[0]因此对于以后的调用保持不变foo

19

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

正如其他人广泛评论的那样,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

Per Brett Slatkin的“有效的Python:59种编写更好的Python的特定方式”,第20项:使用None和文档字符串指定动态默认参数(第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]

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


17

这里的解决方案是:

  1. 使用None作为默认值(或随机数object),以及交换机上,在运行时创建自己的价值观; 要么
  2. 使用a lambda作为默认参数,并在try块中调用它以获取默认值(这是lambda抽象用于的事情)。

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


16

当我们这样做时:

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]

指定a呼叫时foo()确保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]

16

我有时会利用此行为来替代以下模式:

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 executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

我用它来实例化访问外部资源的客户端类,还用于创建字典或用于记忆的列表。

由于我认为这种模式并不为人所知,因此我做了简短的评论,以防止将来发生误解。


2
我更喜欢添加一个用于记忆的装饰器,然后将记忆缓存放入函数对象本身。
Stefano Borini,2015年

本示例不会替代您显示的更复杂的模式,因为您_make_singleton在默认参数示例中的def时间调用,而在全局示例中的调用时间调用。真正的替换将使用某种可变框作为默认参数值,但是参数的添加使您有机会传递备用值。
Yann Vernier

15

您可以通过替换对象来解决这个问题(并因此替换范围):

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

丑陋,但是行得通。


3
如果您使用自动文档生成软件来记录该函数期望的参数类型,那么这是一个很好的解决方案。如果a为None,则放置a = None,然后将a设置为[]不会帮助读者一目了然。
Michael Scott Cuthbert

很棒的主意:重新绑定该名称可确保它永远不会被修改。我真的很喜欢
holdenweb

这正是做到这一点的方法。Python不会复制该参数,因此,您需要明确地复制该参数。拥有副本后,您可以根据需要进行修改,而不会产生任何意外的副作用。
马克·兰瑟姆

13

可能确实是:

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

坚持上述两个功能,并且仍然提出另一点是完全一致的:

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

其他答案,或者至少其中一些答案得分为1和2,而不是3,或者得分为3,淡化得分为1和2。但是所有三个答案都是正确的。

的确,在此处中途更换马匹可能会造成重大损坏,并且通过更改Python以直观地处理Stefano的开头代码段可能会产生更多问题。确实可能是一个非常了解Python内部知识的人可以解释后果的雷区。然而,

现有的行为不是Python的,Python是成功的,因为很少有语言违反任何地方的最小惊讶原则 附近这很糟糕。根除它是否明智是一个真正的问题。这是一个设计缺陷。如果您通过尝试找出行为来更好地理解该语言,那么可以说C ++可以完成所有这些工作,甚至更多。通过导航(例如)细微的指针错误,您学到了很多东西。但这不是Python风格的:关心Python足以在这种行为面前持之以恒的人是被该语言吸引的人,因为Python比其他语言具有更少的惊喜。当涉猎者和好奇的人成为Pythonista者时,他们惊讶地发现需要花很少的时间才能完成某项工作-不是因为设计漏洞-我的意思是隐藏的逻辑难题-消除了被Python吸引的程序员的直觉因为它可行


6
-1虽然防御性的角度来看,这不是一个答案,并且我不同意它。太多特殊异常会导致其自身的极端情况。
Marcin

3
因此,在Python中,每次调用函数时都将[]的默认参数保留为[]更有意义。
Christos Hayward

3
而且,将默认参数设置为None,然后在函数设置的正文中,如果arguments == None,则认为是一个不幸的惯用法:参数= []?人们常常想要一个幼稚的新手会期望这种习惯用法是不幸的,如果您分配f(argument = []),参数将自动默认为[]的值吗?
Christos Hayward

3
但是在Python中,该语言的部分精神是您不必进行过多的深入研究。无论您对排序,big-O和常量了解得很少,array.sort()都可以工作。举一个无数的例子之一,Python在数组排序机制中的美丽之处在于您不需要深入研究内部结构。换句话说,Python的魅力在于通常不需要深入研究实现以使某些东西可以正常工作。并且有一种解决方法(...如果参数==无:参数= []),则失败。
Christos Hayward

3
作为独立语句,该语句的x=[]意思是“创建一个空列表对象,并将名称'x'绑定到该对象。” 因此,在中def f(x=[]),还将创建一个空列表。它并不总是绑定到x,因此它绑定到默认代理。之后,当调用f()时,默认值被拖出并绑定到x。由于是空列表本身被松散了,所以无论是否有任何卡在里面,该列表都是唯一可以绑定到x的东西。怎么会这样呢?
杰里B,

10

这不是设计缺陷。绊倒这个的人做错了什么。

我看到3种情况,您可能会遇到此问题:

  1. 您打算修改参数作为函数的副作用。在这种情况下,没有默认参数是没有意义的。唯一的例外是,当您滥用参数列表以具有函数属性(例如)时cache={},根本就不会期望使用实际参数来调用函数。
  2. 您打算保留该参数不变,但您无意中对其做了修改。那是一个错误,修复它。
  3. 您打算修改在函数内部使用的参数,但是并不希望修改在函数外部可见。在这种情况下,无论是否为默认值,都需要复制该参数!Python不是按值调用的语言,因此它不能为您创建副本,您需要对其进行明确说明。

问题中的示例可能属于类别1或3。奇怪的是,它同时修改了传递的列表并返回了它;您应该选择其中一个。


诊断是“做错了事”。就是说,我认为有时候= None模式不是有用的,但是通常在这种情况下,如果您传递了可变参数,您就不想修改(2)。该cache={}模式实际上是一个只采访的解决方案,在真正的代码,你可能想@lru_cache
安迪·海登

9

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

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

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return 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 it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

只需将功能更改为:

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

7

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

简介。首先,python中有两种类型的数据类型,一种是简单的基本数据类型,例如数字,另一种是对象。其次,当将数据传递给参数时,python按值传递基本数据类型,即,将值的本地副本传递给局部变量,但按引用传递对象,即指向对象的指针。

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

[]是一个对象,因此python将[]的引用传递给a,即,a仅是指向[]的指针,该指针作为对象位于内存中。[]只有一个副本,但是有很多引用。对于第一个foo(),通过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中,item更改为[],即指向另一个具有不同地址的对象。

====== 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(1)时,foo(2,[])中的[]与默认参数[]的内容相同,但是它们的地址却不同!由于我们显式提供了参数,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", i
    UI.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


5
您的最后一点是错误的。试试看,您会发现它x[7]()9
邓肯

2
“ python按值传递基本数据类型,即,将值的本地副本复制到本地变量”是完全错误的。令我惊讶的是,有人显然可以很好地了解Python,却对基本原理有如此可怕的误解。:-(
Veky

6

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


定义一个函数影响两个范围:该范围定义包含的功能,并执行范围由包含的功能。尽管很清楚块是如​​何映射到作用域的,但问题是在哪里def <name>(<args=defaults>):属于:

...                           # defining scope
def 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 time
    parameter = default if parameter is None else parameter
    ...

“一致性已经说了“在定义时”:def <name>(<args=defaults>):在定义时最好也评估所有其他内容。我认为结论不是从前提得出的。仅仅因为两件事在同一行并不意味着它们应该在同一范围内进行评估。default与该行的其余部分不同:这是一个表达式。评估表达式与定义函数是一个非常不同的过程。
LarsH

@LarsH函数定义在Python 评估。无论是从语句(def)还是表达式(lambda)都不会改变创建函数意味着评估-尤其是对其签名的评估。默认值是函数签名的一部分。这并不意味着必须立即评估默认值-例如,可能不需要类型提示。但这当然表明他们应该这样做,除非有充分的理由不这样做。
MiyaGiyagi '19

好的,创建一个函数在某种意义上意味着求值,但是显然并不是在定义时就求值该函数中的每个表达式。大多数不是。对我而言,尚不清楚在什么意义上签名在定义时特别“评估”,而不是对功能主体“进行评估”(解析为合适的表示形式)。而函数主体中的表达式显然没有得到完整的评估。从这个角度来看,一致性意味着签名中的表达式也不应该被“完全”评估。
LarsH

我并不是说您错了,只是您的结论并不仅仅来自一致性。
LarsH

@LarsH默认值既不是正文的一部分,我也不主张一致性是唯一标准。您可以提出建议,以澄清答案吗?
MiyaGiyagi '19

3

其他所有答案都解释了为什么这实际上是一种不错的期望行为,或者为什么无论如何您都不需要这样做。Mine适用于那些固执己见的人,他们想行使自己的权利将语言屈服于自己的意愿,而不是反过来。

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

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

现在,让我们使用此装饰器重新定义函数:

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

foo() # '[5]'
foo() # '[5]' -- as desired

这对于带有多个参数的函数特别整洁。相比:

# the 'correct' approach
def 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
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

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

foo(a=[4])

装饰器可以进行调整以允许这样做,但是我们将其留给读者练习;)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.