函数结果与可变默认参数的串联

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)

同样,您可以执行所有操作,因为您是将对同一对象的引用串联在一起。

  • +所返回的新对象绝对是这里的关键,它是如此微妙 (6认同)
  • 很好地提醒了为什么可变的默认参数是一个很大的禁忌。 (3认同)
  • @HenryYik这是因为list .__ add__是如何实现的。第一个f()实例化列表对象,我们称其为l1。然后调用“ l1 .__ add __(f())”,因此需要首先对“ f()”求值,这将更改与“ l1”共享的引用。然后l1 .__ add __(l2)完成,返回新对象。 (3认同)
  • 查看`dis(lambda:f()+ f()+ f())`之后,函数`f`在执行add之前被调用了两次。好答案顺便说一句。 (2认同)

Eev*_*vee 5

这是一种思考的方法,可能有帮助:

函数是数据结构。用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中那样有意义地检查默认值。