“Least Astonishment” and the Mutable Default Argument
任何修补python足够长时间的人都被以下问题咬(或撕成碎片):
1 2 3 | def foo(a=[]): a.append(5) return a |
python新手希望这个函数总是返回一个只有一个元素的列表:
1 2 3 4 5 6 7 8 9 | >>> foo() [5] >>> foo() [5, 5] >>> foo() [5, 5, 5] >>> foo() [5, 5, 5, 5] >>> foo() |
我的一位经理曾经第一次遇到这个特性,他称之为语言的"戏剧性设计缺陷"。我回答说,这一行为有一个潜在的解释,如果你不理解其内部结构,这确实是非常令人困惑和意想不到的。但是,我无法(对自己)回答以下问题:在函数定义而不是在函数执行时绑定默认参数的原因是什么?我怀疑经验丰富的行为是否有实际用途(谁真的在C中使用了静态变量,而没有滋生虫子?)
编辑:
巴泽克举了一个有趣的例子。结合您的大多数意见和尤塔的特别意见,我进一步阐述了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | >>> def a(): ... print("a executed") ... return [] ... >>> >>> def b(x=a()): ... x.append(5) ... print(x) ... a executed >>> b() [5] >>> b() [5, 5] |
在我看来,设计决策似乎与将参数范围放在哪里相关:在函数内部还是与它"一起"呢?
在函数内部进行绑定意味着,当调用函数(而不是定义函数)时,
实际行为更为一致:当执行该行时,将对该行的所有内容进行评估,这意味着在函数定义中。
实际上,这不是一个设计缺陷,也不是因为内部或性能。这仅仅是因为Python中的函数是一流的对象,而不仅仅是一段代码。
一旦你这样想,它就完全有意义了:一个函数是一个正在根据其定义进行评估的对象;默认参数是一种"成员数据",因此它们的状态可能会从一个调用改变到另一个,就像在任何其他对象中一样。
在任何情况下,effbot在python的默认参数值中都能很好地解释这种行为的原因。我发现它非常清晰,我真的建议阅读它,以便更好地了解对象的工作原理。
假设您有以下代码
1 2 3 4 | fruits = ("apples","bananas","loganberries") def eat(food=fruits): ... |
当我看到eat的声明时,最不令人吃惊的是,如果没有给出第一个参数,它将等于tuple
但是,在代码的后面,我会做一些类似的事情
1 2 3 | def some_random_function(): global fruits fruits = ("blueberries","mangos") |
然后,如果默认参数是在函数执行时绑定的,而不是在函数声明时绑定的,那么我会惊讶(以非常糟糕的方式)发现结果已经改变了。在我看来,这比发现你上面的
真正的问题在于可变变量,所有语言在某种程度上都有这个问题。这里有个问题:假设在Java中,我有以下代码:
1 2 3 4 5 | 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? |
现在,我的地图是在放入地图时使用
您的例子是一个很好的例子,在这个例子中,新来的python用户会感到惊讶和被咬。但我认为,如果我们"修复"这个问题,那么只会造成一个不同的情况,相反,他们会被咬,而且一个会更不直观。此外,在处理可变变量时,情况总是如此;您总是遇到这样的情况:根据编写的代码,某人可以直观地期望一个或相反的行为。
我个人喜欢python当前的方法:当定义了函数并且该对象总是默认值时,将计算默认函数参数。我想他们可以使用一个空列表来处理特殊情况,但是这种特殊的大小写会引起更大的惊讶,更不用说向后不兼容了。
AFAICS还没有人发布文档的相关部分:
Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same"pre-computed" value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function [...]
我对python解释器的内部工作一无所知(我也不是编译器和解释器方面的专家),所以如果我提出任何不敏感或不可能的建议,不要怪我。
如果python对象是可变的,我认为在设计默认参数时应该考虑到这一点。实例化列表时:
1 | a = [] |
您希望得到一个由a引用的新列表。
为什么a=[]应该在
1 | def x(a=[]): |
在函数定义上而不是在调用时实例化一个新列表?这就像你在问"如果用户没有提供参数,然后实例化一个新的列表,就像它是由调用者生成的一样"。我认为这是模棱两可的:
1 | def x(a=datetime.datetime.now()): |
用户,是否希望a默认为定义或执行x时对应的日期时间?在本例中,与前一个一样,我将保持相同的行为,就好像默认参数"assignment"是函数的第一条指令(在函数调用时调用了datetime.now())。另一方面,如果用户想要定义时间映射,他可以写:
1 2 | b = datetime.datetime.now() def x(a=b): |
我知道,我知道:这是一个终结。或者,python可以提供一个关键字来强制定义时间绑定:
1 | def x(static a=b): |
好吧,原因很简单,绑定是在代码执行时完成的,函数定义是执行的,好吧…当函数被定义时。
比较一下:
1 2 3 4 5 | class BananaBunch: bananas = [] def addBanana(self, banana): self.bananas.append(banana) |
此代码遭受完全相同的意外事件。香蕉是一个类属性,因此,当您向它添加内容时,它会添加到该类的所有实例中。原因完全相同。
这只是"它是如何工作的",使它在函数情况下工作的不同可能会很复杂,在类情况下可能不可能,或者至少会减慢对象实例化的速度,因为您必须保留类代码并在创建对象时执行它。
是的,这是出乎意料的。但是一旦一分钱掉下来,它就完全符合Python的一般工作方式。事实上,它是一个很好的教学辅助工具,一旦你理解了为什么会发生这种情况,你就会更好地摸索Python。
也就是说,它应该在任何优秀的Python教程中都有突出的特点。因为正如你所说,每个人迟早都会遇到这个问题。
我曾经认为在运行时创建对象是更好的方法。我现在不太确定,因为你确实丢失了一些有用的特性,尽管这可能值得,不管只是为了防止新手混淆。这样做的缺点是:
1。性能
1 2 | def foo(arg=something_expensive_to_compute())): ... |
如果使用了调用时间计算,那么每次使用函数时都会在没有参数的情况下调用昂贵的函数。您要么在每次调用上付出昂贵的代价,要么需要在外部手动缓存值,污染您的名称空间并添加冗长的内容。
2。强制绑定参数
一个有用的技巧是在创建lambda时将lambda的参数绑定到变量的当前绑定。例如:
1 | funcs = [ lambda i=i: i for i in range(10)] |
这将返回返回0、1、2、3…分别。如果行为改变了,它们将把
否则,实现这一点的唯一方法是使用i绑定创建一个进一步的闭包,即:
1 2 | def make_func(i): return lambda: i funcs = [make_func(i) for i in range(10)] |
三。反省
考虑代码:
1 2 | def foo(a='test', b=100, c=[]): print a,b,c |
我们可以使用
1 2 | >>> inspect.getargspec(foo) (['a', 'b', 'c'], None, None, ('test', 100, [])) |
这些信息对于文档生成、元编程、装饰器等非常有用。
现在,假设违约行为可以改变,这相当于:
1 2 3 4 5 6 | _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(
给定一个简单的小函数
1 2 | >>> def func(a = []): ... a.append(5) |
当Python遇到它时,它要做的第一件事就是编译它,以便为此函数创建一个
所以,让我们做一些内省,前后检查列表如何在函数对象中展开。我用
1 2 3 | >>> def func(a = []): ... a.append(5) ... |
在python执行这个定义之后,它将使用任何指定的默认参数(这里是
1 2 | >>> func.__defaults__ ([],) |
好的,所以一个空的列表作为
现在让我们执行这个函数:
1 | >>> func() |
现在,让我们再看一下那些
1 2 | >>> func.__defaults__ ([5],) |
惊讶的?对象内的值更改!对函数的连续调用现在只需附加到嵌入的
1 2 3 | >>> func(); func(); func() >>> func.__defaults__ ([5, 5, 5, 5],) |
所以,你已经知道了,这个"缺陷"发生的原因,是因为默认参数是函数对象的一部分。这里没有什么奇怪的事情,只是有点奇怪。
解决这个问题的常见方法是使用
1 2 3 4 | def func(a = None): # or: a = [] if a is None else a if a is None: a = [] |
由于函数体每次都重新执行,如果没有为
为了进一步验证
1 2 3 4 5 6 | >>> def func(a = []): ... a.append(5) ... return id(a) >>> >>> id(func.__defaults__[0]) == func() True |
所有的一切都有内省的力量!
*要验证在编译函数期间python是否计算默认参数,请尝试执行以下操作:
1 2 | def bar(a=input('Did you just see me without calling the function?')): pass # use raw_input in Py2 |
正如您将注意到的,在构建函数并将其绑定到名称
Python防御5分
简单性:行为简单如下:大多数人只会掉进这个陷阱一次,而不是几次。
一致性:Python总是传递对象,而不是名称。显然,默认参数是函数的一部分标题(不是函数体)。因此,应该对其进行评估在模块加载时(并且仅在模块加载时,除非嵌套),而不是在函数调用时。
有用性:正如弗雷德里克·伦德在他的解释中指出的那样在"python中的默认参数值"中,当前行为对于高级编程非常有用。(谨慎使用。)
足够的文档:在最基本的python文档中,教程中,这个问题被大声宣布为第一节中的"重要警告""关于定义函数的更多信息"。警告甚至用黑体字,它很少应用于标题之外。RTFM:阅读精细手册。
元学习:陷入陷阱实际上是有帮助的时刻(至少如果你是一个反思性的学习者)因为你随后会更好地理解这一点上面的"一致性",这将教你很多关于Python的知识。
这种行为很容易解释为:
所以:
1 2 3 4 5 | def x(a=0, b=[], c=[], d=0): a = a + 1 b = b + [1] c.append(1) print a, b, c |
你要问的是为什么:
1 2 | def func(a=[], b = 2): pass |
内部不等同于:
1 2 3 4 5 6 7 8 | 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)"可变默认参数"的问题通常是一个特殊的例子,说明:所有有此问题的函数在实际参数上也会遇到类似的副作用问题,这违背了函数式编程的规则,通常是不可考虑的,应该同时固定两者。
例子:
1 2 3 4 5 6 7 8 9 | 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] |
解决方案:副本一个绝对安全的解决方案是先对输入对象执行
1 2 3 4 | def foo(a=[]): a = a[:] # a copy a.append(5) return a # or everything safe by one line:"return a + [5]" |
许多内置可变类型都有一种复制方法,如
类似so问题的示例问题
1 2 3 4 5 6 7 8 9 10 | 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] |
它不应该保存在该函数返回的实例的任何公共属性中。(假设实例的私有属性不应根据约定从该类或子类之外进行修改。即
结论:输入参数对象不应就地修改(变异),也不应绑定到函数返回的对象中。(如果我们喜欢没有副作用的编程,这是强烈推荐的。参见wiki关于"副作用"(前两段与此上下文相关)。)
2)只有当对实际参数的副作用是必需的,而对默认参数是不需要的,那么有用的解决方案是
3)在某些情况下,默认参数的可变行为是有用的。
这实际上与默认值没有任何关系,除了在编写具有可变默认值的函数时,它通常会出现一种意外的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 | >>> 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] |
这段代码中没有默认值,但您会遇到完全相同的问题。
问题是,当调用者不期望时,
您的原始
如果在计算某个东西的过程中需要破坏性地操纵一个局部临时变量,并且需要从一个参数值开始操纵,那么就需要制作一个副本。
这是一个性能优化。由于这个功能,您认为这两个函数调用中哪一个更快?
1 2 3 4 5 | 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 2 3 4 5 | 0 LOAD_GLOBAL 0 (print_tuple) 3 CALL_FUNCTION 0 6 POP_TOP 7 LOAD_CONST 0 (None) 10 RETURN_VALUE |
1 2 3 4 5 6 | 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 |
I doubt the experienced behavior has a practical use (who really used static variables in C, without breeding bugs ?)
如您所见,在使用不可变的默认参数时有一个性能优势。如果它是一个经常调用的函数,或者默认参数需要很长时间来构造,那么这可能会有所不同。另外,请记住,python不是c。在c中,有相当多的自由常数。在python中,您没有这个好处。
已经很忙了,但从我在这里读到的内容来看,以下内容帮助我认识到它是如何在内部工作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 |
最短的答案可能是"定义就是执行",因此整个参数没有严格意义。作为一个更人为的例子,您可以引用:
1 2 3 4 | def a(): return [] def b(x=a()): print x |
希望这足以证明在执行
不过,我同意,当您尝试使用默认构造函数时,这是一个很好的方法。
python:可变的默认参数
默认参数在函数编译为函数对象时进行计算。当被函数使用时,该函数多次使用时,它们是并保持不变的对象。
当它们是可变的时,当它们发生变异(例如,通过向其中添加元素)时,它们在连续调用中保持变异。
它们之所以保持变异,是因为它们每次都是同一个物体。
等效代码:由于在编译和实例化函数对象时列表绑定到函数,因此:
1 2 | def foo(mutable_default_argument=[]): # make a list the default argument """function that uses a list""" |
几乎完全等同于:
1 2 3 4 5 6 | _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都是相同的,
- 注意到当第二次调用使用它的函数时,列表保持不变,
- 观察从源代码打印输出的顺序(我为您方便地编号):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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() |
用
1 2 3 4 5 6 7 8 9 | 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用户的常规说明:但这就是为什么对新用户通常的指令是创建这样的默认参数:
1 2 3 | def example_function_2(default_kwarg=None): if default_kwarg is None: default_kwarg = [] |
这将使用none-singleton作为sentinel对象来告诉函数我们是否得到了默认参数以外的参数。如果我们没有得到参数,那么我们实际上想使用一个新的空列表,
正如控制流的教程部分所说:
If you don’t want the default to be shared between subsequent calls,
you can write the function like this instead:
1
2
3
4
5 def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
使用无的简单解决方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | >>> 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教程中进行了描述。试图为只读类属性赋值时:
...all variables found outside of the innermost scope are
read-only (an attempt to write to such a variable will simply create a
new local variable in the innermost scope, leaving the identically
named outer variable unchanged).
回顾最初的例子,并考虑以上几点:
1 2 3 | def foo(a=[]): a.append(5) return a |
这里,
在不覆盖默认值的情况下调用
现在,将其与文档中关于模拟其他语言的默认参数行为的示例进行比较,以便每次执行函数时都使用函数签名默认值:
1 2 3 4 5 | 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 时,foo.func_defs[0] 保持不变。
我将演示将默认列表值传递给函数的替代结构(它同样适用于字典)。
正如其他人广泛评论的那样,列表参数在定义时绑定到函数,而不是在执行时绑定到函数。因为列表和字典是可变的,所以对该参数的任何更改都将影响对该函数的其他调用。因此,对函数的后续调用将接收此共享列表,该列表可能已被对函数的任何其他调用所更改。更糟糕的是,两个参数同时使用了这个函数的共享参数,而忽略了另一个参数所做的更改。
错误的方法(可能是…):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 |
您可以使用
1 2 3 4 5 | >>> id(a) 5347866528 >>> id(b) 5347866528 |
根据BrettSlatkin的"有效的python:59编写更好的python的特定方法",第20项:使用
The convention for achieving the desired result in Python is to
provide a default value ofNone and to document the actual behaviour
in the docstring.
此实现确保对函数的每个调用要么接收默认列表,要么接收传递给函数的列表。
首选方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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] |
"错误的方法"可能存在合法的使用案例,程序员希望共享默认列表参数,但这更可能是例外情况,而不是规则。
这里的解决方案是:
第二个选项很好,因为该函数的用户可以传入一个可调用的,它可能已经存在(例如
我有时利用这种行为来替代以下模式:
1 2 3 4 5 6 7 8 9 | singleton = None def use_singleton(): global singleton if singleton is None: singleton = _make_singleton() return singleton.use_me() |
如果
1 2 3 | # _make_singleton() is called only once when the def is executed def use_singleton(singleton=_make_singleton()): return singleton.use_me() |
我使用它来实例化访问外部资源的客户机类,以及创建用于内存化的dict或列表。
因为我不认为这种模式是众所周知的,所以我确实做了一个简短的评论来防止将来的误解。
当我们这样做时:
1 2 | def foo(a=[]): ... |
…如果调用方不传递a的值,我们将参数
为了使讨论更简单,我们暂时为未命名的列表命名。埃多克斯1〔1〕怎么样?
1 2 | def foo(a=pavlo): ... |
在任何时候,如果调用者没有告诉我们
如果
所以这就是你看到的(记住,
1 2 | >>> foo() [5] |
现在,
再次调用
1 2 | >>> foo() [5, 5] |
调用
1 2 3 4 5 | >>> ivan = [1, 2, 3, 4] >>> foo(a=ivan) [1, 2, 3, 4, 5] >>> ivan [1, 2, 3, 4, 5] |
因此,
1 2 | >>> foo() [5, 5, 5] |
您可以通过替换对象来绕过这个问题(因此也可以用范围来绑定):
1 2 3 4 | def foo(a=[]): a = list(a) a.append(5) return a |
很难看,但很管用。
可能是真的:
它完全一致地保留了上述两个特性,并且仍然提出了另一个观点:
其他答案,或者至少其中的一些,要么得到1分和2分,但不是3分,要么得到3分和1分和2分。但这三个都是真的。
这可能是真的,在中游切换马会要求显著的中断,并且通过更改python来直观地处理stefano的开头片段可能会产生更多的问题。而且,了解Python内部结构的人能够很好地解释后果的雷区,这可能是真的。然而,
现有的行为不是Python式的,而Python之所以成功,是因为很少有语言违反了在这附近任何地方都不令人吃惊的原则。这是一个真正的问题,是否是明智的根除它。这是一个设计缺陷。如果你试图通过追踪行为来更好地理解语言,我可以说C++做了所有这些和更多;你通过导航,例如微妙的指针错误学到了很多。但这并不是Python式的:那些足够关心Python并坚持面对这种行为的人是那些被语言所吸引的人,因为与其他语言相比,Python的惊喜要少得多。当达布勒和好奇的人惊讶于花了这么少的时间让某个东西工作时,他们会变成pythonistas——不是因为一个设计fl——我的意思是,隐藏的逻辑难题——这违背了程序员的直觉,他们被python吸引,因为它只是工作。
这个"错误"给了我很多加班时间!但我开始看到它的潜在用途(但我还是希望它在执行时出现)
我会给你一个有用的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 ' '.join(errors) main() |
打印以下内容
1 2 3 4 | Attempt to fix it Attempt to fix it by another way Attempt to fix it Attempt to fix it by another way |
我认为这个问题的答案在于python如何将数据传递给参数(传递值或引用),而不是可变的,或者python如何处理"def"语句。
简单介绍。首先,在Python中有两种数据类型,一种是简单的基本数据类型,如数字,另一种数据类型是对象。第二,当将数据传递给参数时,python按值传递基本数据类型,即将值的本地副本传递给局部变量,但按引用传递对象,即指向对象的指针。
承认以上两点,让我们解释一下python代码发生了什么。这仅仅是因为通过对象的引用传递,但与可变/不可变无关,或者可以说,"def"语句在定义时只执行一次这一事实无关。
[]是一个对象,所以python将[]的引用传递给
为了进一步验证我的答案,让我们看看另外两个代码。
第2号
1 2 3 4 5 6 7 8 9 | 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] |
第3号
1 2 3 4 5 6 7 | def foo(x, items=[]): items.append(x) return items foo(1) # returns [1] foo(2,[]) # returns [2] foo(3) # returns [1,3] |
对foo(1)的调用使items指向一个地址为11111111的列表对象[]。在后续的foo函数中,列表的内容更改为1,但是地址没有更改,仍然是11111111。然后foo(2,[])就来了。尽管在调用foo(1)时foo(2,[])中的[]与默认参数[]的内容相同,但它们的地址不同!由于我们显式地提供了参数,所以
从上面的解释可以看出,在接受的答案中推荐的effbot网页没有给出这个问题的相关答案。更重要的是,我认为effbot网页上的一点是错误的。我认为ui.button的代码是正确的:
1 2 3 4 | for i in range(10): def callback(): print"clicked button", i UI.Button("button %s" % i, callback) |
每个按钮都可以有一个不同的回调函数,显示不同的
1 2 3 4 5 | x=[] for i in range(10): def callback(): print(i) x.append(callback) |
如果我们执行
这不是设计缺陷。任何一个在这上面绊倒的人都在做错事。
我发现有3种情况您可能会遇到此问题:
问题中的例子可能属于1类或3类。奇怪的是,它同时修改传递的列表并返回它;您应该选择其中一个。
只需将函数更改为:
1 2 3 4 5 | def notastonishinganymore(a = []): '''The name is just a joke :)''' a = a[:] a.append(5) return a |
tldr:define时间默认值是一致的,并且更具表现力。
定义一个函数会影响两个作用域:包含该函数的定义作用域和该函数包含的执行作用域。虽然很清楚块是如何映射到作用域的,但问题是
1 2 3 | ... # defining scope def name(parameter=default): # ??? ... # execution scope |
由于
现在,什么时候评估
一致性已经表示"在定义上":
这两个选项也不相等:如果在定义时评估
1 2 3 4 5 6 | def name(parameter=defined): # set default at definition time ... def name(parameter=None): # delay default until execution time parameter = [] if parameter is None else parameter ... |
1 2 3 4 5 6 7 8 9 10 11 12 | >>> def a(): >>> print"a executed" >>> return [] >>> x =a() a executed >>> def b(m=[]): >>> m.append(5) >>> print m >>> b(x) [5] >>> b(x) [5, 5] |
每一个其他的答案都解释了为什么这实际上是一个好的和想要的行为,或者为什么你无论如何都不应该需要它。我的是那些顽固的人,他们想行使他们的权利,使语言屈从于他们的意志,而不是相反的方式。
我们将使用一个修饰符来"修复"这个行为,该修饰符将复制默认值,而不是为每个保留在默认值的位置参数重用相同的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 |
现在,让我们使用这个修饰器重新定义我们的函数:
1 2 3 4 5 6 7 | @sanify def foo(a=[]): a.append(5) return a foo() # '[5]' foo() # '[5]' -- as desired |
对于采用多个参数的函数来说,这尤其简单。比较:
1 2 3 4 5 6 7 8 9 | # 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 |
具有
1 2 3 4 | # the nasty decorator hack @sanify def bar(a=[], b=[], c=[]): # wow, works right out of the box! |
重要的是要注意,如果您尝试使用关键字args,上面的解决方案会中断,例如:
1 | foo(a=[4]) |
装饰师可以调整以允许这样做,但我们将此作为练习留给读者;)
建筑
在函数调用中分配默认值是一种代码味道。
1 2 | def a(b=[]): pass |
这是一个不起作用的函数的签名。不仅仅是因为其他答案所描述的问题。我不想在这里谈这个。
这个功能的目的是做两件事。创建一个新的列表,并执行一个功能,最有可能出现在该列表中。
做两件事的函数是坏函数,正如我们从干净的代码实践中学习到的。
利用多态性来解决这个问题,我们将扩展python列表或将其包装在一个类中,然后对其执行我们的函数。
但是等等,你说,我喜欢我的一句台词。
好吧,猜猜看。代码不仅仅是控制硬件行为的一种方法。这是一种方式:
与其他开发人员进行通信,使用相同的代码。
能够在新的需求出现时改变硬件的行为。
在两年后再次拿起代码进行上述更改后,能够理解程序的流程。
不要把定时炸弹留给你自己以后取。
把这个函数分成两个部分,我们需要一个类
1 2 3 4 5 6 7 8 | class ListNeedsFives(object): def __init__(self, b=None): if b is None: b = [] self.b = b def foo(): self.b.append(5) |
被执行
1 2 3 | a = ListNeedsFives() a.foo() a.b |
为什么这比将上面所有的代码混合成一个函数要好呢?
1 2 3 4 5 | def dontdothis(b=None): if b is None: b = [] b.append(5) return b |
为什么不这样做?
除非您的项目失败,否则您的代码将继续存在。最有可能的是,你的功能将不仅仅是这样。制造可维护代码的正确方法是将代码分离成具有适当限制范围的原子部分。
类的构造函数对于任何进行过面向对象编程的人来说都是一个非常常见的组件。将处理列表实例化的逻辑放在构造函数中,可以使理解代码所做工作的认知负载变小。
方法
在返回一个独立的列表时,您可以假设做任何您想做的事情都是安全的。但它可能不是,因为它也由对象