任何修补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
在某种意义上,该行将是"混合"的(部分绑定)函数对象)将在定义时发生,并在函数调用时发生部分(默认参数的赋值).
实际行为更加一致:执行该行时,该行的所有内容都会得到评估,这意味着在函数定义中.
实际上,这不是设计缺陷,并不是因为内部或性能.
它只是因为Python中的函数是第一类对象,而不仅仅是一段代码.
一旦你以这种方式思考,那么它就完全有意义了:一个函数是一个被定义的对象; 默认参数是一种"成员数据",因此它们的状态可能会从一个调用更改为另一个调用 - 与任何其他对象完全相同.
无论如何,Effbot 对Python中的默认参数值中出现这种行为的原因有一个非常好的解释.
我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理.
假设您有以下代码
fruits = ("apples", "bananas", "loganberries") def eat(food=fruits): ...
当我看到吃的声明时,最令人惊讶的是认为如果没有给出第一个参数,它将等于元组 ("apples", "bananas", "loganberries")
但是,假设后面的代码,我会做类似的事情
def some_random_function(): global fruits fruits = ("blueberries", "mangos")
然后,如果默认参数在函数执行而不是函数声明中被绑定,那么我会惊讶地发现水果已被改变(以非常糟糕的方式).这比发现foo
上面的函数改变列表更令人惊讶的IMO .
真正的问题在于可变变量,并且所有语言都在某种程度上存在这个问题.这是一个问题:假设在Java中我有以下代码:
StringBuffer s = new StringBuffer("Hello World!");
Map counts = new HashMap();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) ); // does this work?
现在,我的地图StringBuffer
在放入地图时是否使用了键的值,还是通过引用存储了键?无论哪种方式,有人感到惊讶; 尝试将对象从Map
使用中取出的值与他们放入的对象相同的人,或者即使他们使用的键实际上是同一个对象而无法检索其对象的人用于将其放入映射的(这实际上是Python不允许其可变内置数据类型用作字典键的原因).
你的例子是一个很好的例子,Python新人会感到惊讶和被咬.但我认为,如果我们"修复"这个,那么这只会产生一种不同的情况,即他们会被咬伤,而那种情况甚至会更不直观.而且,在处理可变变量时总是如此; 你总是遇到一些情况,根据他们正在编写的代码,某人可能直观地期望一种或相反的行为.
我个人喜欢Python当前的方法:默认函数参数在定义函数时进行评估,并且该对象始终是默认值.我想他们可以使用空列表进行特殊情况,但这种特殊的外壳会引起更多的惊讶,更不用说倒退不兼容了.
AFAICS尚未发布文档的相关部分:
执行函数定义时,将评估默认参数值.这意味着当定义函数时,表达式被计算一次,并且每次调用使用相同的"预先计算"值.这对于理解默认参数是可变对象(例如列表或字典)时尤其重要:如果函数修改对象(例如,通过将项附加到列表),则默认值实际上被修改.这通常不是预期的.解决这个问题的方法是使用None作为默认值,并在函数体中显式测试它[...]
我对Python解释器内部工作一无所知(我也不是编译器和解释器方面的专家)所以如果我提出任何不可知或不可能的建议,请不要责怪我.
如果python对象是可变的,我认为在设计默认参数时应该考虑到这一点.实例化列表时:
a = []
你希望得到一个由a引用的新列表.
为什么a = [] in
def x(a=[]):
在函数定义上实例化一个新列表而不是在调用上?就像你问"用户是否提供参数然后实例化一个新列表并使用它就好像它是由调用者生成"一样.我认为这是模棱两可的:
def x(a=datetime.datetime.now()):
用户,你想一个默认为相应的,当你正在定义或执行到datetime X?在这种情况下,与前一个一样,我将保持相同的行为,就好像默认参数"assignment"是函数的第一条指令(在函数调用上调用datetime.now()).另一方面,如果用户想要定义时间映射,他可以写:
b = datetime.datetime.now() def x(a=b):
我知道,我知道:这是一个封闭.或者,Python可能会提供一个关键字来强制定义时绑定:
def x(static a=b):
嗯,原因很简单,在执行代码时完成绑定,并且执行函数定义,以及......定义函数时.
比较一下:
class BananaBunch: bananas = [] def addBanana(self, banana): self.bananas.append(banana)
此代码遭受完全相同的意外事件.bananas是一个类属性,因此,当您向其添加内容时,它会添加到该类的所有实例中.原因完全一样.
这只是"如何工作",并且在功能案例中使其工作方式可能很复杂,并且在类的情况下可能不可能,或者至少减慢对象实例化的速度,因为你必须保持类代码并在创建对象时执行它.
是的,这是出乎意料的.但是一旦下降了,它就完全适合Python的工作方式.事实上,它是一个很好的教学辅助工具,一旦你理解为什么会发生这种情况,你就会更好地理解python.
这说它应该在任何优秀的Python教程中占据突出地位.因为正如你所提到的,每个人迟早都会遇到这个问题.
我曾经认为在运行时创建对象将是更好的方法.我现在不太确定,因为你确实失去了一些有用的功能,尽管它可能是值得的,不管只是为了防止新手混淆.这样做的缺点是:
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=[]
但是,我们已经失去了内省的能力,并且看到了默认参数是什么.因为没有构造对象,所以我们不能在没有实际调用函数的情况下获取它们.我们能做的最好的事情是存储源代码并将其作为字符串返回.
我真的很惊讶,没有人执行被Python提供了精辟的内省(2
和3
上可调用适用).
给定一个简单的小函数func
定义为:
>>> def func(a = []): ... a.append(5)
当Python遇到它时,首先要做的是编译它以便code
为这个函数创建一个对象.完成此编译步骤后,Python会计算*,然后将默认参数([]
此处为空列表)存储在函数对象本身中.正如最佳回答所述:列表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
.
简单性:从以下意义上说,行为很简单:大多数人只陷入一次,而不是几次.
一致性:Python 总是传递对象,而不是名称.显然,默认参数是函数标题的一部分(不是函数体).因此,应该在模块加载时评估(并且仅在模块加载时,除非嵌套),而不是在函数调用时.
实用性:正如Frederik Lundh在他对"Python中的默认参数值"的解释中指出的那样,当前的行为对于高级编程非常有用.(谨慎使用.)
足够的文档:在最基本的Python文档,教程中,该问题在"更多关于定义函数"一节的第一小节中 被大声宣布为"重要警告".警告甚至使用粗体,这很少在标题之外应用.RTFM:阅读精细手册.
元学习:陷入陷阱实际上是一个非常有用的时刻(至少如果你是一个反思学习者),因为你随后会更好地理解上面的"一致性"这一点,这将教会你很多关于Python的知识.
这种行为很容易解释为:
函数(类等)声明只执行一次,创建所有默认值对象
一切都通过参考传递
所以:
def x(a=0, b=[], c=[], d=0): a = a + 1 b = b + [1] c.append(1) print a, b, c
a
不会改变 - 每个赋值调用都会创建新的int对象 - 打印新对象
b
不会更改 - 新数组是从默认值构建并打印的
c
更改 - 对同一对象执行操作 - 并打印
你问的是为什么这个:
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)的情况,我们将忽略它.
换句话说,为什么不存储它们中的每一个,而不是评估默认参数,并在调用函数时对它们进行评估?
答案可能就在那里 - 它会有效地将每个具有默认参数的函数转换为闭包.即使它全部隐藏在解释器中而不是一个完整的闭包,数据也必须存储在某个地方.它会更慢并且使用更多内存.
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]
解决方案:一个副本
的绝对安全解决方案是copy
或deepcopy
输入,然后再去做任何与复制对象.
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 = []
更多..
3)在某些情况下,默认参数的可变行为很有用.
这实际上与默认值无关,除了它在您使用可变默认值编写函数时经常出现意外行为.
>>> 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
它是显式传入还是获得默认值.您的代码应该单独保留可变参数,除非从context/name/documentation明确指出应该修改参数.使用作为参数传递的可变值作为本地临时值是一个非常糟糕的主意,无论我们是否使用Python,以及是否涉及默认参数.
如果你需要在计算某些东西时破坏性地操纵一个本地临时,并且你需要从一个参数值开始你的操作,你需要复制一份.
已经很忙的主题,但从我在这里读到的内容,以下内容帮助我了解它是如何在内部工作的:
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
这是一种性能优化.作为此功能的结果,您认为这两个函数调用中的哪一个更快?
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中使用静态变量,没有繁殖错误?)
正如你所看到的,是用一成不变的默认参数时提高性能.如果它是一个经常调用的函数,或者默认参数需要很长时间来构造,这可能会有所不同.另外,请记住Python不是C.在C中你有几乎是免费的常量.在Python中你没有这个好处.
最短的答案可能是"定义是执行",因此整个论证没有严格意义.作为一个更人为的例子,你可以引用这个:
def a(): return [] def b(x=a()): print x
希望它足以表明在def
语句执行时不执行默认参数表达式并不容易或没有意义,或两者兼而有之.
我同意当你尝试使用默认构造函数时,这是一个问题.
在将函数编译为函数对象时,将计算默认参数.当函数使用该函数多次时,它们是并且保持相同的对象.
当它们是可变的时,当变异时(例如,通过向其添加元素),它们在连续调用时保持变异.
他们保持变异,因为他们每次都是同一个对象.
由于列表在编译和实例化函数对象时绑定到函数,因此:
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: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执行模型,那么它就变得非常期待.
但这就是为什么对新用户的通常指令是创建这样的默认参数:
def example_function_2(default_kwarg=None): if default_kwarg is None: default_kwarg = []
这使用None singleton作为sentinel对象来告诉函数我们是否得到了除默认值之外的参数.如果我们没有参数,那么我们实际上想要使用一个新的空列表[]
,作为默认值.
正如关于控制流的教程部分所说:
如果您不希望在后续调用之间共享默认值,则可以编写如下函数:
def f(a, L=None): if L is None: L = [] L.append(a) return L
如果您考虑以下因素,这种行为就不足为奇了:
分配尝试时只读类属性的行为,以及
函数是对象(在接受的答案中解释得很好).
(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]
,因为该属性是只读的.
Per (1),在本地范围内创建一个也命名的新局部变量L
,并用于函数调用的其余部分.foo.func_defs[0]
因此未来的调用保持不变foo
.
使用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]
这里的解决方案是:
使用None
作为默认值(或随机数object
),以及交换机上,在运行时创建自己的价值观; 要么
使用a lambda
作为默认参数,并在try块中调用它以获取默认值(这是lambda抽象的用途).
第二个选项很好,因为函数的用户可以传入一个可调用的,这可能已经存在(例如a type
)
我将演示一个替代结构,将默认列表值传递给函数(它对字典同样有效).
正如其他人已经广泛评论的那样,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:编写更好的Python的59种方法",第20项:使用None
和文档字符串来指定动态默认参数(p.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]
可能存在"错误方法"的合法用例,其中程序员希望共享默认列表参数,但这更可能是规则之外的例外.
当我们这样做时:
def foo(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]
我有时会利用此行为作为以下模式的替代方法:
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()
我已经将它用于实例化访问外部资源的客户端类,也用于创建用于memoization的dicts或列表.
由于我不认为这种模式是众所周知的,所以我做了一个简短的评论,以防止未来的误解.
你可以通过替换对象(因此与范围的关系)来绕过这个:
def foo(a=[]): a = list(a) a.append(5) return a
丑陋,但它的工作原理.
可能是这样的:
有人正在使用每种语言/库功能,并且
在这里改变行为是不明智的,但是
坚持上述两个特征完全一致,但仍然提出另一个观点:
这是一个令人困惑的功能,它在Python中是不幸的.
其他答案,或者至少其中一些答案要么分1和2而不是3分,要么分3分和低分1分和2分.但这三个都是真的.
在这里切换马匹可能会要求严重破损,并且通过更改Python以直观地处理Stefano的开放片段可能会产生更多问题.而且,熟悉Python内部人员的人可能会解释一个后果的雷区.然而,
现有的行为不是Pythonic,Python是成功的,因为很少有关于语言的内容违反了最接近这一点的最不惊讶的原则.这是一个真正的问题,无论是否根除它是明智的.这是一个设计缺陷.如果你通过试图追踪行为来更好地理解语言,我可以说C++完成所有这些以及更多; 通过导航,例如微妙的指针错误,你可以学到很多东西.但这不是Pythonic:那些关心Python足以坚持这种行为的人是那些被语言所吸引的人,因为Python比其他语言的意外要少得多.Dabblers和好奇的人成为Pythonistas,因为他们对于让事情变得有效所花费的时间感到惊讶 - 不是因为设计因素 - 我的意思是隐藏的逻辑谜题 - 削弱了被Python吸引的程序员的直觉因为它只是工作.
这不是设计缺陷.任何绊倒此事的人都在做错事.
我看到有3个案例可能会遇到这个问题:
您打算将参数修改为函数的副作用.在这种情况下,拥有默认参数永远不会有意义.唯一的例外是当您滥用参数列表以具有函数属性时,例如cache={}
,并且您不希望根据实际参数调用该函数.
您打算离开参数修改,但你不小心没有修改.这是一个错误,修复它.
您打算修改在函数内部使用的参数,但不希望修改在函数外部可见.在这种情况下,您需要复制参数,无论是否为默认值!Python不是一种按值调用的语言,因此它不会为您制作副本,您需要明确它.
问题中的示例可能属于类别1或3.奇怪的是,它都修改了传递的列表并返回它; 你应该选择一个或另一个.
这个"虫子"给了我很多加班时间!但我开始看到它的潜在用途(但我还是喜欢它在执行时,仍然)
我会给你我看到的一个有用的例子.
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
只需将功能更改为:
def notastonishinganymore(a = []): '''The name is just a joke :)''' a = a[:] a.append(5) return a
我认为这个问题的答案在于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时,都会评估项目(而不是仅评估一次的某些答案)为None,要清楚,是None的引用(或地址).然后在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(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", 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
.
TLDR:定义时间默认值是一致的,并且更具表现力。
定义一个函数影响两个范围:该范围定义包含的功能,并执行范围由包含的功能。尽管很清楚块是如何映射到作用域的,但问题是在哪里def
属于:
... # defining scope def name(parameter=default): # ??? ... # execution scope
该def name
零件必须在定义范围内进行评估- name
毕竟我们希望在那里可用。仅在内部评估函数将使其无法访问。
由于parameter
是一个常量名,因此我们可以与同时“评估”它def name
。这还有一个优势,那就是它可以生成具有已知签名的功能name(parameter=...):
,而不是裸露的签名name(...):
。
现在,什么时候评估default
?
一致性已经说了“在定义时”:def
在定义时最好也评估其他所有内容。延迟其中的一部分将是令人惊讶的选择。
两种选择都不相等:如果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 ...