Ben*_*tte 47 python python-3.x
假设一个函数具有可变的默认参数:
def f(l=[]):
l.append(len(l))
return l
Run Code Online (Sandbox Code Playgroud)
如果我运行此命令:
def f(l=[]):
l.append(len(l))
return l
print(f()+["-"]+f()+["-"]+f()) # -> [0, '-', 0, 1, '-', 0, 1, 2]
Run Code Online (Sandbox Code Playgroud)
或这个:
def f(l=[]):
l.append(len(l))
return l
print(f()+f()+f()) # -> [0, 1, 0, 1, 0, 1, 2]
Run Code Online (Sandbox Code Playgroud)
代替以下内容,这将更合乎逻辑:
print(f()+f()+f()) # -> [0, 0, 1, 0, 1, 2]
Run Code Online (Sandbox Code Playgroud)
为什么?
For*_*Bru 52
这实际上很有趣!
众所周知,l
函数定义中的列表仅在此函数的定义中初始化一次,并且对于此函数的所有调用,将仅此列表的一个副本。现在,该函数将修改此列表,这意味着对该函数的多次调用将多次修改完全相同的对象。这是第一重要部分。
现在,考虑添加以下列表的表达式:
f()+f()+f()
Run Code Online (Sandbox Code Playgroud)
根据运营商优先权定律,这等效于以下内容:
(f() + f()) + f()
Run Code Online (Sandbox Code Playgroud)
...与此完全相同:
temp1 = f() + f() # (1)
temp2 = temp1 + f() # (2)
Run Code Online (Sandbox Code Playgroud)
这是第二重要部分。
列表的添加会产生一个新对象,而无需修改其任何参数。这是第三重要部分。
现在,让我们将我们所知道的结合在一起。
[0]
如您所料,在上面的第1行中,第一个调用返回。[0, 1]
如您所料,第二个调用返回。等一下!修改后,该函数将一遍又一遍地返回完全相同的对象(而不是其副本!)。这意味着第一个调用返回的对象现在也变为了[0, 1]
!这就是为什么temp1 == [0, 1] + [0, 1]
。
但是,加法的结果是一个全新的对象,因此[0, 1, 0, 1] + f()
与相同[0, 1, 0, 1] + [0, 1, 2]
。请注意,第二个列表再次正是您希望函数返回的列表。添加时会发生相同的事情f() + ["-"]
:这将创建一个新 list
对象,以便其他任何调用f
都不会干扰该对象。
您可以通过串联两个函数调用的结果来重现此问题:
>>> f() + f()
[0, 1, 0, 1]
>>> f() + f()
[0, 1, 2, 3, 0, 1, 2, 3]
>>> f() + f()
[0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]
Run Code Online (Sandbox Code Playgroud)
同样,您可以执行所有操作,因为您是将对同一对象的引用串联在一起。
这是一种思考的方法,可能有帮助:
函数是数据结构。用def
块创建一个,与用块创建一个类型class
或用方括号创建一个列表的方式几乎相同。
数据结构中最有趣的部分是在调用函数时运行的代码,但是默认参数也是其中的一部分!事实上,你可以检查这两个代码,并在Python默认参数,通过对功能属性:
>>> def foo(a=1): pass
...
>>> dir(foo)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', ...]
>>> foo.__code__
<code object foo at 0x7f114752a660, file "<stdin>", line 1>
>>> foo.__defaults__
(1,)
Run Code Online (Sandbox Code Playgroud)
(一个更好的接口是inspect.signature
,但是它所做的只是检查那些属性。)
因此,这会修改列表的原因:
>>> def foo(a=1): pass
...
>>> dir(foo)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', ...]
>>> foo.__code__
<code object foo at 0x7f114752a660, file "<stdin>", line 1>
>>> foo.__defaults__
(1,)
Run Code Online (Sandbox Code Playgroud)
与修改列表的原因完全相同:
def f(l=[]):
l.append(len(l))
return l
Run Code Online (Sandbox Code Playgroud)
在这两种情况下,您都在突变属于某个父结构的列表,因此更改自然也将在父结构中可见。
请注意,这是Python专门做出的设计决定,在语言中并不是固有的。JavaScript最近了解了默认参数,但是将它们视为要在每次调用时重新评估的表达式-本质上,每个默认参数都是自己的小函数。优点是JS没有这个陷阱,但是缺点是您无法像在Python中那样有意义地检查默认值。