任何人对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?)
修改:
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
行是"混合的",即部分(功能对象的)绑定将在定义时发生,而部分(默认参数的分配)将在函数调用时发生。
实际行为更加一致:执行该行时,将评估该行的所有内容,即在函数定义时进行评估。
实际上,这不是设计缺陷,也不是因为内部因素或性能问题。
仅仅是因为Python中的函数是一流的对象,而不仅仅是一段代码。
一旦您开始思考这种方式,那便完全有意义:函数是根据其定义进行评估的对象;默认参数属于"成员数据",因此它们的状态可能会从一个调用更改为另一个调用-完全与其他任何对象一样。
无论如何,Effbot在默认参数值中都很好地解释了这种行为的原因。在Python中使用。
我发现它很清晰,因此我建议阅读此书以更好地了解函数对象的工作原理。
假设您具有以下代码
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当前的方法:定义函数时会评估默认函数参数,而该对象始终是默认对象。我想他们可以使用空列表作为特殊情况,但是这种特殊的大小写会引起更多的惊讶,更不用说向后不兼容了。
文档的相关部分:
执行函数定义时,默认参数值从左到右求值。这意味着,在定义函数时,表达式将被求值一次,并且相同的"预先计算"值用于每个呼叫。这对于理解默认参数何时是可变对象(例如列表或字典)尤为重要:如果函数修改了该对象(例如,通过将项目附加到列表),则默认值实际上已被修改。这通常不是预期的。解决此问题的一种方法是使用
None
作为默认值,并在函数主体中明确测试它,例如:def whats_on_the_telly(penguin=None): if penguin is None: penguin = [] penguin.append("property of the zoo") return penguin
我对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):
嗯,原因很简单,就是在执行代码时完成绑定,然后执行函数定义,好吧……在定义函数时。
比较:
class BananaBunch:
bananas = []
def addBanana(self, banana):
self.bananas.append(banana)
此代码遭受完全相同的意外事件。 bananas是一个类属性,因此,当您向其中添加内容时,它将被添加到该类的所有实例中。原因是完全一样的。
这只是"它是如何工作的",而使其在函数情况下以不同的方式工作可能会很复杂,而在类情况下则可能是不可能的,或者至少会大大减慢对象实例化,因为您必须保持类代码,并在创建对象时执行。
是的,这是意外的。但是一旦一分钱下降,它就完全适合Python的一般工作方式。实际上,这是一种很好的教学手段,一旦您了解了为什么会发生这种情况,您就会更好地使用python。
那表明它应该在任何好的Python教程中都非常突出。因为正如您提到的,每个人迟早都会遇到这个问题。
我真的很惊讶,没有人对可调用对象执行Python提供的深刻内省(2
和3
适用)。
给出一个简单的小功能func
定义为:
>>> def func(a = []):
... a.append(5)
当Python遇到它时,它要做的第一件事就是对其进行编译,以便为此函数创建一个code
对象。完成此编译步骤后, Python 求值 *,然后将默认参数(此处为空列表[]
)存储在功能对象本身。正如上面提到的最高答案:列表a
现在可以被认为是函数func
的成员。
因此,让我们做一下自省,前后检查功能对象内部的列表扩展方式。我为此使用Python3.x
,对于Python 2同样适用(在Python 2中使用__defaults__
或func_defaults
;是的,两个相同的名称)。
>>> def func(a = []):
... a.append(5)
...
Python执行此定义后,它将采用指定的任何默认参数(此处为a=[]
)和将其填充到功能对象的__defaults__
属性中(相关部分:可调用项):
>>> func.__defaults__
([],)
好,因此,空列表作为__defaults__
中的单个条目,与预期的一样。
现在让我们执行此功能:
>>> func()
现在,让我们再次看看那些__defaults__
:
>>> func.__defaults__
([5],)
惊讶吗?对象内部的值改变了!现在,对该函数的连续调用将简单地追加到该嵌入的list
对象:
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
因此,出现'flaw'的原因是,因为默认参数是函数对象的一部分。这里没有什么奇怪的事情,这一切都令人惊讶。
解决此问题的常见方法是使用None
作为默认值,然后在函数体内进行初始化:
def func(a = None):
# or: a = [] if a is None else a
if a is None:
a = []
由于函数主体每次都会重新执行,因此如果未为a
传递任何参数,则始终会得到一个新的空列表。
要进一步验证__defaults__
中的列表与函数func
中使用的列表相同,您只需更改函数以返回id
a
的code>。然后,将其与__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()
。
我曾经认为在运行时创建对象是更好的方法。我现在不太确定,因为您确实失去了一些有用的功能,尽管不管是为了防止新手混淆,还是值得的。这样做的缺点是:
1。效果
def foo(arg=something_expensive_to_compute())):
...
如果使用调用时评估,则每次使用不带参数的函数时都会调用昂贵的函数。您可能会为每次调用付出昂贵的代价,或者需要在外部手动缓存该值,从而污染您的名称空间并增加冗长程度。
2。强制绑定参数
一个有用的技巧是在创建lambda时将lambda的参数绑定到变量的 current 绑定。例如:
funcs = [ lambda i=i: i for i in range(10)]
这将返回分别返回0、1、2、3 ...的函数列表。如果更改了行为,则它们会将i
绑定到i的 call-time 值,因此您将获得一个全部返回了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 始终传递对象,而不是名称。显然,默认参数是函数标题的一部分(而不是函数主体)。因此,应该在模块加载时(除非嵌套,仅在模块加载时)评估它,而不是在函数调用时评估它。
有用性:正如Frederik Lundh在他对" Python中的默认参数值" ,当前行为对于高级编程非常有用。 (请谨慎使用。)
足够的文档:在最基本的Python文档(该教程)中,该问题在 first中被大声宣布为"重要警告" 。 小节"有关定义函数的更多信息" 。警告甚至使用黑体字,很少在标题之外使用。 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)所谓的"可变默认参数"问题通常是一个特殊的示例,它说明:
"所有具有此问题的功能也遭受实际参数的类似副作用问题
strong>,"
这违反了函数编程的规则,通常是不希望的,应该将两者固定在一起。
示例:
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]
解决方案:一个副本
一个绝对安全的解决方案是 复制
或
<首先对输入对象进行code>
deepcopy
首先对输入对象进行code>,然后对副本执行任何操作。
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()
进行更彻底的复制(如果可变对象是由可变对象组成的,后者很有用) 。有些对象从根本上是基于副作用的,例如"文件"对象,并且不能通过复制有意义地进行复制。 复制
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]
不应同时将其保存在此函数返回的实例的任何 public 属性中。 (假设实例的 private 属性不应按照惯例从此类或子类的外部进行修改。即_var1
是私有属性)
结论:
输入参数对象不应就地修改(突变),也不应将其绑定到函数返回的对象中。 (如果强烈建议您进行编程,而没有副作用,则强烈建议。请参见有关"副作用"的Wiki < / a>(在此情况下,前两段是无关紧要的。)
2)
仅当需要对实际参数产生副作用但对默认参数没有副作用时,有用的解决方案是def...(var1=None):
如果var1为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
,无论它是显式传入还是具有默认值。除非上下文/名称/文档中明确指出应修改参数,否则您的代码应仅保留可变参数。将传入的可变值作为参数用作本地临时对象是一个极坏的主意,无论我们是否使用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中使用了静态变量,而没有滋生bug?)
如您所见,使用不可变默认参数时,具有性能。如果这是一个经常调用的函数,或者默认参数需要花费很长时间来构造,则可能会有所不同。另外,请记住,Python不是C。在C中,您拥有几乎免费的常量。在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
这是一个演示-每次被
引用时,您可以验证它们是否是同一对象 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()
并使用pythonexample.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执行模型,那么就可以预期了。
但这就是为什么通常向新用户说明的是创建这样的默认参数的原因:
def example_function_2(default_kwarg=None):
if default_kwarg is None:
default_kwarg = []
这将None单例用作哨兵对象,以告诉函数我们是否获得了默认值以外的参数。如果没有参数,则实际上我们想使用一个新的空列表[]
作为默认列表。
正如关于控制流的教程部分所述: / p>
如果您不希望在后续调用之间共享默认设置,则可以这样编写函数:
def f(a, L=None): if L is None: L = [] L.append(a) return L
最短的答案可能是"定义就是执行",因此整个论点没有严格意义。作为更人为的示例,您可以引用以下内容:
def a(): return []
def b(x=a()):
print x
希望足以证明,在def
语句的执行时间不执行默认参数表达式不是一件容易的事,或者没有道理,或者两者兼而有之。
不过,当您尝试使用默认构造函数时,我同意这是一个陷阱。
使用"无"的简单解决方法
>>> 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]
如果考虑以下因素,这种行为就不足为奇了:
(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]
(无
)在本地范围内以L
可用。
- 在
L=[]
上,赋值无法在foo.func_defs[0]
处成功进行,因为该属性是只读的。
- 每个(1), 都会在本地范围内创建一个新的本地变量,也称为
L
,用于其余的函数调用。因此,foo.func_defs[0]
对于以后调用foo
保持不变。
我将演示将默认列表值传递给函数的替代结构(与字典同样有效)。
正如其他人广泛评论的那样,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
和Docstrings指定动态默认参数(第48页)
在Python中获得所需结果的约定是提供默认值
None
,并在文档字符串中记录实际行为。
此实现可确保对函数的每次调用都可以接收默认列表,也可以将列表传递给函数。
首选方法:
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]
"错误方法"可能存在合法的用例,程序员可能希望使用这些错误的方法来共享默认列表参数,但这比规则更可能是例外。
这里的解决方案是:
None
作为默认值(或nonce object
),并在运行时将其打开以创建您的值;或lambda
作为默认参数,并在try块中调用它以获取默认值(这是lambda抽象的目的)。第二个选项很好,因为该函数的用户可以传入一个可调用的,可能已经存在(例如type
)
当我们这样做时:
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 executed
def use_singleton(singleton=_make_singleton()):
return singleton.use_me()
我已使用它来实例化访问外部资源的客户端类,还用于创建用于记忆的字典或列表。
由于我认为这种模式并不为人所知,因此我发表了简短的评论,以防止将来发生误会。
您可以通过替换对象来解决这个问题(并因此替换范围):
def foo(a=[]):
a = list(a)
a.append(5)
return a
丑陋,但是可行。
可能是这样的:
同时保留上述两个功能并保持另一点是完全一致的:
其他答案,或者至少其中一些答案得分为1和2,但不是3,或者得分为3,淡化得分为1和2。但这三个答案都是正确的。
的确,在此处中途更换马匹会造成重大损坏,并且通过更改Python以直观地处理Stefano的开头片段可能会产生更多问题。也许确实很了解Python内部知识的人可以解释后果的雷区。 但是,
现有的行为不是Python式的,Python是成功的,因为很少有关于该语言的行为违反了 near 这么严重的地方的最小惊讶原则。根除它是否明智是一个真正的问题。这是一个设计缺陷。如果您通过尝试找出行为来更好地理解该语言,那么可以说C ++可以完成所有这些工作,甚至更多。通过导航(例如)细微的指针错误,您学到了很多东西。但这不是Python风格的:关心Python足以在这种行为面前持之以恒的人是被该语言吸引的人,因为Python比其他语言具有更少的惊喜。当涉猎者和好奇者成为一名Pythonista使用者时,他们惊讶地发现需要花很少的时间才能完成某项工作,而不是因为设计漏洞-我的意思是隐藏的逻辑难题-消除了被Python吸引的程序员的直觉因为它正常工作。
这不是设计缺陷。任何绊倒的人都在做错事。
在3种情况下,我发现您可能会遇到此问题:
cache={}
,根本就不会调用带有实际参数的函数。问题中的示例可能属于类别1或3。奇怪的是,它既修改了传递的列表并返回了它;您应该选择一个。
这个" 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
只需将功能更改为:
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(),通过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)的列表对象[]。列表的内容更改为 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
。
TLDR:定义时间默认值是一致的,并且更具表现力。
定义功能会影响两个范围:定义范围包含功能和执行范围由包含功能。虽然很清楚块是如何映射到作用域的,但问题是def
属于哪里:
... # defining scope
def name(parameter=default): # ???
... # execution scope
defname
部分必须在定义范围内进行评估-毕竟,我们希望name
在那里可用。仅在内部评估函数会使其无法访问。
由于parameter
是一个常量名称,因此我们可以与defname
同时"评估"它。这还有一个优点,就是它生成具有已知签名的函数,即name(parameter=...):
,而不是裸露的name(...):
。
现在,何时评估默认
?
一致性已经说了"在定义时":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
...
其他所有答案都解释了为什么这实际上是一种不错的期望行为,或者为什么您不应该这样。 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])
可以对装饰器进行调整以允许这样做,但是我们将其留给读者练习;)