Good uses for mutable function argument default values?
在Python中,将可变对象设置为函数中参数的默认值是一个常见的错误。下面是大卫·古德这篇精彩的文章中的一个例子:
1 2 3 4 5 6 7
| >>> def bad_append(new_item, a_list=[]):
a_list.append(new_item)
return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two'] |
这就是为什么会发生这种情况的原因。
现在我的问题是:这个语法有一个好的用例吗?
我的意思是,如果每个遇到它的人都犯同样的错误,揭穿它,理解问题,并从中试图避免它,那么这种语法有什么用呢?
- 参见stackoverflow.com/questions/1132941/…
- 对此,我所知道的最好的解释是在链接的问题中:函数是第一类对象,就像类一样。类具有可变的属性数据;函数具有可变的默认值。
- stackoverflow.com/questions/2639915/…
- 这种行为不是一种"设计选择"——它是语言工作方式的结果——从简单的工作原理开始,尽可能少的例外。在某种程度上对我来说,当我开始"用Python思考"时,这种行为变得自然了——如果没有发生,我会感到惊讶。
- 我也很好奇。这个例子在网络上随处可见,但它没有意义——要么你想改变传递的列表,而使用默认值是没有意义的,要么你想返回一个新的列表,你应该在进入函数后立即进行复制。我无法想象这两种方法都有用的情况。
- fwiw我用它们来回答一个问题:一个函数在一个循环中只执行一次的有效方法。
- 我刚刚遇到了一个更现实的例子,没有我上面抱怨的问题。默认值是类的__init__函数的一个参数,它被设置为一个实例变量;这是一个完全有效的操作,并且所有的操作都会因可变的默认值而严重错误。stackoverflow.com/questions/43768055/…
- 5年后:好的。似乎这个bug唯一的好用法就是memoization,它无论如何都可以用@functools.lru_cache来完成。叹息…
- @markransom:很好的例子。它看起来足够正确和简单,它可以在许多其他语言中工作,但在Python中失败得很惨。
- @EricDuminil它不是Python中的一个bug,它只是一些需要对内部的更深入理解的东西,而不是大多数人愿意接受的。一旦你摸索过这些内在的东西,这是完全有道理的,但是它每天都会让那些天真的人感到困惑——这完全是不直觉的。
- @markransom:根据您的定义,在(确定性的)计算机上不会有任何错误。当你花足够的时间去探索内部时,每个错误都是有意义的。让我们诚实地把这种行为称为Python中极少数的设计缺陷之一。
您可以使用它在函数调用之间缓存值:
1 2 3 4
| def get_from_cache(name, cache={}):
if name in cache: return cache[name]
cache[name] = result = expensive_calculation()
return result |
但是通常这类事情在类中做得更好,因为您可以拥有其他属性来清除缓存等。
- …或者是一个记忆装饰师。
- @functools.lru_cache(maxsize=None)
- @katrielalex lru缓存在python 3.2中是新的,所以不是每个人都可以使用它。
- 仅供参考,现在有backports.functools_lru_cache pypi.python.org/pypi/backports.functools_lru_cache
- 如果您有不可显示的值,则lru_cache不可用。
1 2 3 4
| import random
def ten_random_numbers(rng=random):
return [rng.random() for i in xrange(10)] |
使用random模块作为默认的随机数生成器,它实际上是一个可变的单例。
- 但这也不是一个非常重要的用例。
- 我认为python的"只获取一次引用"和python的"每个函数调用一次查找random"在行为上没有区别。两个都使用同一个对象。
规范答案是:http://effbot.org/zone/default-values.htm
它还提到了可变默认参数的3个"好"用例:
- 将局部变量绑定到回调中外部变量的当前值
- 缓存/内存化
- 全局名称的本地重新绑定(用于高度优化的代码)
也许您不改变可变参数,但需要可变参数:
1 2 3 4
| def foo(x, y, config={}):
my_config = {'debug': True, 'verbose': False}
my_config.update(config)
return bar(x, my_config) + baz(y, my_config) |
(是的,我知道在这种情况下,你可以使用config=(),但我发现这不太清楚,也不太一般。)
编辑(澄清):可变默认参数问题是更深层次设计选择的一个症状,即默认参数值存储为函数对象上的属性。你可能会问为什么要做出这样的选择;像往常一样,这些问题很难正确回答。但它确实有很好的用途:
优化性能:
1
| def foo(sin=math.sin): ... |
在闭包中而不是变量中获取对象值。
1 2 3 4
| callbacks = []
for i in range(10):
def callback(i=i): ...
callbacks.append(callback) |
- 整数和内置函数不可变!
- @乔纳森:在剩下的例子中仍然没有可变的默认参数,还是我没有看到它?
- @乔纳森:我的意思不是这些是可变的。这是因为系统python用来存储默认参数——在编译时定义的函数对象上——是有用的。这意味着可变的默认参数问题,因为在每个函数调用上重新计算参数将使技巧无效。
- @凯特里亚克斯:好的,但是请在你的回答中这样说,你假设论点必须重新评估,并且你要说明为什么那样会很糟糕。nit pick:默认参数值不会在编译时存储,而是在执行函数定义语句时存储。
- @沃尔夫拉姆:是的:P!尽管这两种情况经常是一致的。