提问人:Stefano Borini 提问时间:7/16/2009 最后编辑:Volker SiegelStefano Borini 更新时间:8/12/2023 访问量:260140
“最小惊讶”和可变的默认参数
"Least Astonishment" and the Mutable Default Argument
问:
任何对 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
实际行为更加一致:该行的所有内容在执行该行时都会被计算,这意味着在函数定义时。
答:
假设您有以下代码
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
当我看到 eat 的声明时,最不令人惊讶的是认为,如果没有给出第一个参数,它将等于元组("apples", "bananas", "loganberries")
但是,假设稍后在代码中,我执行类似操作
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
然后,如果默认参数是在函数执行时绑定的,而不是在函数声明时绑定的,我会惊讶地(以一种非常糟糕的方式)发现结果已经改变。这比发现您上面的函数正在改变列表更令人惊讶。foo
真正的问题在于可变变量,所有语言在某种程度上都存在这个问题。这里有一个问题:假设在 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?
现在,我的映射是使用键放入映射时的值,还是通过引用存储键?无论哪种方式,都会有人感到惊讶;要么是试图使用与他们放入对象相同的值将对象从中取出的人,要么是似乎无法检索对象的人,即使他们使用的键实际上与用于将其放入映射中的对象相同(这实际上就是 Python 不允许将其可变内置数据类型用作字典键的原因)。StringBuffer
Map
你的例子是一个很好的例子,Python 新手会感到惊讶和咬伤。但我认为,如果我们“修复”了这一点,那么这只会造成一种不同的情况,即它们会被咬伤,而且这种情况会更不直观。此外,在处理可变变量时总是如此;你总是会遇到这样的情况,即有人可以根据他们正在编写的代码直观地期望一种或相反的行为。
我个人喜欢 Python 当前的方法:默认函数参数在定义函数时计算,并且该对象始终是默认值。我想他们可以使用空列表进行特殊情况,但这种特殊大小写会引起更大的惊讶,更不用说向后不兼容了。
评论
("blueberries", "mangos")
some_random_function()
fruits
eat()
global
eat
原因很简单,绑定是在执行代码时完成的,并且执行了函数定义,嗯......定义函数时。
比较一下:
class BananaBunch:
bananas = []
def addBanana(self, banana):
self.bananas.append(banana)
这段代码也遭遇了完全相同的意外事件。Bananas 是一个类属性,因此,当你向它添加内容时,它会被添加到该类的所有实例中。原因完全一样。
它只是“它是如何工作的”,在函数情况下让它以不同的方式工作可能会很复杂,在类情况下可能是不可能的,或者至少会大大减慢对象实例化的速度,因为你必须保留类代码并在创建对象时执行它。
是的,这是出乎意料的。但是,一旦一分钱掉下来,它就完全符合 Python 的一般工作方式。事实上,这是一个很好的教具,一旦你理解了为什么会发生这种情况,你就会更好地理解 python。
也就是说,它应该在任何好的 Python 教程中占据突出地位。因为正如你提到的,每个人迟早都会遇到这个问题。
评论
property
self.attribute = value
这种行为很容易解释为:
- 函数(类等)声明只执行一次,创建所有默认值对象
- 一切都是通过引用传递的
所以:
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
更改 - 对同一对象执行操作 - 并打印
评论
__iadd__
你要问的是为什么会这样:
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) 的情况,我们将忽略它。
换句话说,为什么不存储每个参数,并在调用函数时评估它们,而不是评估默认参数呢?
一个答案可能就在那里——它会有效地将每个带有默认参数的函数变成闭包。即使这一切都隐藏在解释器中,而不是完全关闭,数据也必须存储在某个地方。它会更慢,并且会占用更多内存。
评论
这是一种性能优化。由于此功能,您认为这两个函数调用中哪一个更快?
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 中,你没有这个好处。
我对 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 {}
我曾经认为在运行时创建对象会是更好的方法。我现在不太确定,因为您确实失去了一些有用的功能,尽管无论为了防止新手混淆,这可能都是值得的。这样做的缺点是:
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=[]
但是,我们已经失去了反省的能力,也无法查看默认参数是什么。由于对象尚未构造,因此如果不实际调用函数,我们就无法获取它们。我们能做的最好的事情就是存储源代码并将其作为字符串返回。
评论
最简短的答案可能是“定义就是执行”,因此整个论点没有严格意义。作为一个更人为的例子,你可以引用这个:
def a(): return []
def b(x=a()):
print x
希望这足以表明在语句执行时不执行默认参数表达式并不容易或没有意义,或两者兼而有之。def
不过,我同意当您尝试使用默认构造函数时,这是一个问题。
可能是真的:
- 有人正在使用每种语言/库功能,并且
- 在这里切换行为是不明智的,但是
坚持上述两个功能并仍然提出另一个观点是完全一致的:
- 这是一个令人困惑的功能,在 Python 中是不幸的。
其他答案,或者至少其中一些答案要么提出第 1 点和第 2 点,但没有提出第 3 点,或者提出第 3 点并淡化第 1 点和第 2 点。但这三个都是真的。
在这里,在中游切换马匹可能会造成严重的损坏,并且通过更改 Python 来直观地处理 Stefano 的开场片段可能会产生更多问题。而且,熟悉 Python 内部结构的人可能确实可以解释后果的雷区。然而
现有的行为不是 Pythonic,而 Python 之所以成功,是因为该语言几乎没有违反最小惊讶原则。这是一个真正的问题,无论将其连根拔起是否明智。这是一个设计缺陷。如果你通过尝试追踪行为来更好地理解语言,我可以说 C++ 可以完成所有这些甚至更多;例如,通过导航细微的指针错误,您可以学到很多东西。但这不是 Pythonic:那些足够关心 Python 的人,在面对这种行为时坚持不懈,是被这门语言所吸引的人,因为 Python 的惊喜比其他语言少得多。涉猎者和好奇者会成为 Pythonistas,因为他们惊讶于让某些东西工作所需的时间如此之短——不是因为设计失误——我的意思是,隐藏的逻辑谜题——这与程序员的直觉背道而驰,他们被 Python 所吸引,因为它只是工作。
评论
x=[]
def f(x=[])
实际上,这不是设计缺陷,也不是因为内部或性能。它只是因为 Python 中的函数是第一类对象,而不仅仅是一段代码。
一旦你这样想,那么它就完全有道理了:函数是一个对象,正在根据其定义进行评估;默认参数是一种“成员数据”,因此它们的状态可能会从一个调用更改为另一个 - 与任何其他对象完全相同。
无论如何,effbot (Fredrik Lundh) 在 Python 中的默认参数值中对这种行为的原因进行了很好的解释。 我发现它非常清楚,我真的建议阅读它以更好地了解函数对象的工作原理。
评论
functions are objects
这实际上与默认值无关,只是在编写具有可变默认值的函数时,它经常出现意外行为。
>>> 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
带有默认参数的原始 ,不应修改它是显式传入还是获取默认值。您的代码应该保留可变参数,除非从上下文/名称/文档中可以清楚地看出应该修改参数。使用可变值作为参数作为本地临时参数传递是一个非常糟糕的主意,无论我们是否在 Python 中,也无论是否涉及默认参数。foo
a
如果你需要在计算过程中破坏性地操纵一个局部临时,并且你需要从一个参数值开始你的操作,你需要创建一个副本。
评论
append
a
cache={}
None
None
这里的解决方案是:
- 用作默认值(或随机数),并打开它以在运行时创建值;或
None
object
- 使用 a 作为默认参数,并在 try 块中调用它以获取默认值(这就是 lambda 抽象的用途)。
lambda
第二个选项很好,因为函数的用户可以传入一个可调用对象,该可调用对象可能已经存在(例如type
)
评论
如果考虑到以下因素,则此行为不足为奇:
- 只读类属性在赋值尝试时的行为,以及
- 函数是对象(在公认的答案中解释得很好)。
(2)的作用已在此线程中广泛介绍。(1)可能是引起惊讶的因素,因为这种行为在来自其他语言时并不“直观”。
(1) 在 Python 类教程中进行了描述。尝试为只读类属性赋值时:
...在最内层范围之外找到的所有变量都是 只读(尝试写入此类变量只会创建一个 在最内层的作用域中新建局部变量,保留相同的 命名外部变量不变)。
回顾一下原来的例子,考虑以上几点:
def foo(a=[]):
a.append(5)
return a
下面是一个对象,是 (可在 上找到)的属性。由于是一个列表,是可变的,因此是 的读写属性。在实例化函数时,它被初始化为签名指定的空列表,只要函数对象存在,它就可用于读取和写入。foo
a
foo
foo.func_defs[0]
a
a
foo
在不覆盖缺省值的情况下进行调用时,将使用 该缺省值 from 。在本例中,用于函数对象的代码范围内。更改 ,它是对象的一部分,在执行 中的代码之间持续存在。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
文档的相关部分:
执行函数定义时,默认参数值从左到右计算。这意味着在定义函数时,表达式将计算一次,并且每次调用都使用相同的“预先计算”值。当默认参数是可变对象(例如列表或字典)时,理解这一点尤其重要:如果函数修改了对象(例如,通过将项目附加到列表),则默认值实际上被修改了。这通常不是预期的。解决此问题的方法是用作默认值,并在函数主体中显式测试它,例如:
None
def whats_on_the_telly(penguin=None): if penguin is None: penguin = [] penguin.append("property of the zoo") return penguin
评论
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]
解决方案:复制 一个绝对安全的解决方案是先复制或深度复制
输入对象,然后对副本执行任何操作。
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 = list(a)
a.append(5)
return a
丑陋,但它有效。
评论
使用 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]
评论
这个“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
评论
errors
我认为这个问题的答案在于 python 如何将数据传递给参数(通过值或引用传递),而不是可变性或 python 如何处理“def”语句。
简要介绍。首先,python 中有两种类型的数据类型,一种是简单的基本数据类型,如数字,另一种数据类型是对象。其次,在将数据传递给参数时,python 按值传递基本数据类型,即将值的本地副本复制到局部变量,但通过引用传递对象,即指向对象的指针。
承认以上两点,我们来解释一下python代码发生了什么。这只是因为对象的引用传递,但与可变/不可变无关,或者可以说是“def”语句在定义时只执行一次。
[] 是一个对象,所以 python 将 [] 的引用传递给 ,即,只是一个指向 [] 的指针,它作为对象存在于内存中。但是,只有一个 [] 副本,其中包含许多引用。对于第一个 foo(),通过 append 方法将列表 [] 更改为 1。但请注意,列表对象只有一个副本,此对象现在变为 1。当运行第二个 foo() 时,effbot 网页上所说的(不再评估项目)是错误的。 被计算为列表对象,尽管现在该对象的内容为 1。这就是引用传递的效果!foo(3) 的结果可以很容易地用同样的方式推导出来。a
a
a
为了进一步验证我的答案,让我们看一下另外两个代码。
====== 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]
[]
是一个对象,所以是(前者是可变的,而后者是不可变的。但可变性与问题无关)。没有在空间的某个地方,但我们知道它在那里,那里只有一个无的副本。因此,每次调用 foo 时,项目都会被评估为“无”(而不是“只计算一次”的答案),明确地说,是 None 的引用(或地址)。然后在 foo 中,item 更改为 [],即指向另一个具有不同地址的对象。None
====== 第 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) 会使 items 指向一个列表对象 [],地址为 11111111。在续集的 foo 函数中,列表的内容改为 1,但地址没有改动,还是11111111。然后 foo(2,[]) 来了。虽然 foo(2,[]) 中的 [] 在调用 foo(1) 时与默认参数 [] 的内容相同,但它们的地址是不同的!由于我们明确提供了参数,因此必须获取这个 new 的地址,比如 2222222,并在进行一些更改后返回它。现在 foo(3) 被执行了。由于提供了 only,因此 items 必须再次采用其默认值。默认值是多少?它是在定义 foo 函数时设置的:位于 11111111 中的列表对象。因此,这些项目被评估为具有元素 1 11111111地址。位于 2222222 处的列表还包含一个元素 2,但它不再由项目指向。因此,附加 3 将构成 [1,3]。items
[]
x
items
从上面的解释中,我们可以看出,被接受的答案中推荐的effbot网页未能给出这个问题的相关答案。更重要的是,我认为effbot网页中的一点是错误的。我认为有关UI的代码。按钮正确:
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)
如果我们执行,我们将按预期得到 7,并将给出 9,另一个值为 。x[7]()
x[9]()
i
评论
x[7]()
9
当我们这样做时:
def foo(a=[]):
...
...如果调用方没有传递 A 的值,我们将参数分配给一个未命名的列表。a
为了简化此讨论,让我们暂时为未命名列表命名。怎么样 ?pavlo
def foo(a=pavlo):
...
在任何时候,如果调用方没有告诉我们是什么,我们就会重用 .a
pavlo
如果是可变的(可修改的),并最终修改它,则我们下次会注意到在不指定 的情况下调用效果。pavlo
foo
foo
a
所以这就是你所看到的(记住,初始化为 []):pavlo
>>> foo()
[5]
现在,是 [5]。pavlo
再次调用会再次修改: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()
我用它来实例化访问外部资源的客户端类,也用它来创建字典或列表以进行记忆。
由于我不认为这种模式是众所周知的,所以我做了一个简短的评论,以防止将来的误解。
评论
_make_singleton
已经很忙的话题,但从我在这里读到的内容来看,以下内容帮助我意识到它在内部是如何工作的:
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
评论
a = a + [1]
a
b = a + [1] ; print id(b)
a.append(2)
+
b
a
id(a)
Python 的 5 点防御
简单性:从以下意义上讲,行为很简单: 大多数人只落入这个陷阱一次,而不是几次。
一致性:Python 始终传递对象,而不是名称。 显然,默认参数是函数的一部分 标题(不是函数正文)。因此,应该对其进行评估 在模块加载时(并且仅在模块加载时,除非嵌套),而不是 在函数调用时。
有用性:正如弗雷德里克·伦德(Frederik Lundh)在他的解释中指出的那样 在“Python 中的默认参数值”中, 当前行为对于高级编程非常有用。 (谨慎使用。
足够的文档:在最基本的 Python 文档中, 教程中,问题被大声宣布为 “有关定义函数的更多信息”一节第一小节中的“重要警告”。 警告甚至使用粗体, 这很少应用于标题之外。 RTFM:阅读精美的手册。
元学习:掉进陷阱其实是很 有帮助的时刻(至少如果你是一个反思性的学习者), 因为你随后会更好地理解这一点 上面的“一致性”,这将 教你很多关于 Python 的知识。
评论
只需将函数更改为:
def notastonishinganymore(a = []):
'''The name is just a joke :)'''
a = a[:]
a.append(5)
return a
评论
我将演示一种将默认列表值传递给函数的替代结构(它同样适用于字典)。
正如其他人所广泛评论的那样,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
根据 Brett Slatkin 的“Effective Python: 59 Specific Ways to Write Better Python”,第 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]
“错误方法”可能存在合法的用例,即程序员打算共享默认列表参数,但这更有可能是例外而不是规则。
你为什么不反省?
我真的很惊讶没有人对可调用对象执行 Python(和 apply)提供的有见地的内省。2
3
给定一个简单的小函数,定义为:func
>>> def func(a = []):
... a.append(5)
当 Python 遇到它时,它要做的第一件事就是编译它,以便为此函数创建一个对象。完成此编译步骤后,Python 会评估*,然后将默认参数(此处为空列表 []
)存储在函数对象本身中。正如上面的答案所提到的:现在可以将列表视为函数的成员。code
a
func
因此,让我们做一些内省,在函数对象内部检查列表是如何扩展的。我为此使用,对于 Python 2,这同样适用(在 Python 2 中使用 or;是的,同一事物的两个名称)。Python 3.x
__defaults__
func_defaults
执行前的功能:
>>> def func(a = []):
... a.append(5)
...
Python 执行此定义后,它将获取指定的任何默认参数(此处),并将它们塞入函数对象的 __defaults__
属性中(相关部分:Callables):a = []
>>> 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
要进一步验证中的列表是否与函数中使用的列表相同,只需更改函数以返回函数体中使用的列表。然后,将它与 (position 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
评论
id(...)
is
is
id(val)
None
__defaults__
__defaults__
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
这不是设计缺陷。任何被此绊倒的人都做错了什么。
我看到有 3 种情况您可能会遇到此问题:
- 您打算修改参数作为函数的副作用。在这种情况下,使用默认参数是没有意义的。唯一的例外是当你滥用参数列表来具有函数属性时,例如 ,你根本不会用实际的参数来调用函数。
cache={}
- 您打算不修改参数,但您不小心修改了它。这是一个错误,修复它。
- 您打算修改参数以在函数内部使用,但不希望在函数外部查看修改。在这种情况下,您需要复制参数,无论它是否是默认的!Python 不是一种按值调用的语言,所以它不会为你制作副本,你需要明确它。
问题中的例子可能属于第 1 类或第 3 类。奇怪的是,它既修改了传递的列表,又返回了它;你应该选择一个或另一个。
评论
@lru_cache
!cache={}
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>):
这两个选项也不等效:如果在定义时进行计算,它仍然会影响执行时间。如果在执行时计算,则不会影响定义时间。选择“at definition”允许表达两种情况,而选择“at execution”只能表达一种情况: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
def
lambda
所有其他答案都解释了为什么这实际上是一种很好且理想的行为,或者为什么您无论如何都不需要它。我的是给那些固执的人,他们想行使自己的权利,使语言屈服于他们的意志,而不是相反。
我们将使用装饰器“修复”此行为,该装饰器将复制默认值,而不是为每个位置参数重复使用相同的实例,保留其默认值。
import inspect
from copy import deepcopy # copy would fail on deep arguments like nested dicts
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(deepcopy(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!
需要注意的是,如果您尝试使用关键字参数,上述解决方案会中断,如下所示:
foo(a=[4])
可以调整装饰器以允许这一点,但我们将其作为读者的练习;)
评论
{"grandparent": {"parent": {"child": "value"}}}
copy
deepcopy
有一种简单的方法可以理解为什么会发生这种情况。
Python 在命名空间中从上到下执行代码。
“内部”只是这一规则的体现。
做出这种选择的原因是“让语言适合你的头脑”。所有奇怪的极端情况都倾向于简化为在命名空间中执行代码:默认不可变、嵌套函数、类(编译完成后有一点修补)、self 参数等。同样,复杂的语法可以用简单的语法来编写:is just .这适用于列表推导式;装饰;元类;和更多。这使您可以近乎完美地看到奇怪的角落。语言适合你的头脑。a.foo(...)
a.lookup('foo').__call__(a,...)
你应该坚持下去。学习 Python 有一段时间对这门语言的不满,但它会变得很舒服。这是我工作过的唯一一种语言,你越看极端情况就越简单。
继续黑客攻击!做笔记。
对于您的特定代码,过于详细:
def foo(a=[]):
a.append(5)
return a
foo()
是一个语句,等同于:
- 开始创建代码对象。
- 现在解释,正如我们所走。是参数 a 的默认值。它一如既往地属于列表类型。
(a=[])
[]
[]
- 将 之后的所有代码编译成 Python 字节码,并将其粘贴到另一个列表中。
:
- 创建可调用字典,参数和代码位于“code”字段中
- 将可调用对象添加到当前命名空间的“foo”字段中。
然后,它转到下一行 .foo()
- 它不是保留字,因此请在命名空间中查找它。
- 调用该函数,该函数将使用列表作为默认参数。开始在其命名空间中执行其字节码。
append
不创建新列表,因此修改了旧列表。
是的,这是 Python 中的设计缺陷
我已经阅读了所有其他答案,但我不相信。这种设计确实违反了最小惊讶原则。
默认值可以设计为在调用函数时计算,而不是在定义函数时计算。Javascript 是这样做到的:
function foo(a=[]) {
a.push(5);
return a;
}
console.log(foo()); // [5]
console.log(foo()); // [5]
console.log(foo()); // [5]
为了进一步证明这是一个设计缺陷,Python 核心开发人员目前正在讨论引入新的语法来解决这个问题。请参阅本文:Python 的后期绑定参数默认值。
为了证明这是一个设计缺陷的更多证据,如果你在谷歌上搜索“Python gotchas”,这个设计在前 9 个 Google 结果(1、2、3、4、5、6、7、8、9)中被提及为一个陷阱,通常是列表中的第一个陷阱。相比之下,如果你在谷歌上搜索“Javascript gotchas”,Javascript 中默认参数的行为甚至一次都不会被提及为陷阱。
顾名思义,Gotchas 违反了最小惊讶原则。他们感到惊讶。鉴于默认参数值的行为存在高级设计,不可避免的结论是 Python 在这里的行为代表了设计缺陷。
我是作为一个热爱 Python 的人这么说的。我们可以成为 Python 的粉丝,并且仍然承认,每个对 Python 的这一方面感到不愉快的人都会感到不愉快的惊讶,因为它是一个真正的“陷阱”。
评论
def
def
foobar = lambda x: print("hi")
hi
print
文档失败
def fn(<mutable object> = [manifest constant]):
此 python 语法用于可变对象的可选初始值设定项。将清单常量称为默认值既错误又令人困惑。特别不幸的是,官方的 python 文档使用了误导性的描述,并且这个令人痛苦的误导性术语已被复制到网络上,包括在这里的问题中。
通过更正文档并不能完全解决问题:人们仍然希望 python 为可变对象实现默认值。但这将是一个开始。
来自命令查询分离的参数
Python 非常尊重命令查询分离的原则。例如,普通赋值被视为语句,而不是表达式,因此不能用作子表达式;许多内置和标准库方法主要通过修改对象来工作时,会返回 None,而不是对象。
我们可以提出一个简单的论点,反对对参数使用可变的默认值,如下所示:
有两种方法可以从函数中获取信息:通过操作它,或者通过修改封闭作用域中的某些变量。
return
但是,“修改封闭范围内的变量”的唯一合理的、可重用的方法是修改其中一个参数。修改函数本身会使界面更加尴尬(并可能导致递归代码出现问题)。修改其他任何内容都需要函数和调用方共享该上下文;这意味着会污染全局命名空间,并再次创建一个笨拙且高度非标准的接口。
默认参数值的要点是能够在没有相应参数的情况下调用函数;因此,我们必须针对发生这种情况的情况进行设计。
假设我们在没有相应参数的情况下调用该函数,因此使用默认值。我们将如何发出变化的信号?默认值的对象不在调用方的作用域中,并且它不是函数的属性;因此,调用方不容易访问它。因此,我们无法通过现在变异的默认值轻松获取信息。
因此,为了传达结果,我们必须提供一些信息。
return
为了使接口保持一致,并避免使用接口的复杂特殊情况,因此该函数应一致地返回计算结果。
但是,由于命令查询分离,这意味着该函数不应修改参数。我们不能两者兼而有之,因为那样的话,“提出问题可能会改变答案”。
由于该函数不会修改参数,因此它不会修改默认值对象。
但是,如果对象不会被修改,则没有理由让它可变。使用可变类型会误导性地暗示要改变值。
在实践中,人们有时会编写默认参数,即使代码不会改变它们。用 替换它很容易,但表示“一个空的、不可变的映射”并非易事。当然,使用(不可变的)作为哨兵,然后显式检查该值,巧妙地回避了这个问题。{}
[]
()
None
评论
[5]
foo([1])
[1, 5]
[5]
[5]