Python 中闭包的可变默认参数的实例化时间

Instantiation time for mutable default arguments of closures in Python

我的理解是,当Python解析一个函数的源代码时,它会把它编译成字节码,但在调用函数之前不会运行这个字节码(这就是为什么非法变量除非您调用该函数,否则函数中的名称不会引发异常。

默认参数 在函数的初始设置期间实例化,但仅在首次调用函数时实例化,无论是否提供参数。默认参数的相同实例用于所有未来的调用,这可以通过使用可变类型作为默认参数来看出。

但是,如果我们将函数放在另一个函数内部,则默认参数现在似乎在每次调用外部函数时都会重新实例化,如下代码所示:

def f(x):
    def g(y, a=[]):
        a.append(y)
        return a

    for y in range(x, x + 2):
        print('calling g from f:', g(y))
    return g(y + 1)

for x in range(2):
    print('calling f from module scope:', f(x))

这会打印出来

calling g from f: [0]
calling g from f: [0, 1]
calling f from module scope: [0, 1, 2]
calling g from f: [1]
calling g from f: [1, 2]
calling f from module scope: [1, 2, 3]

这是否意味着每次调用f时,都会重建g的字节码?这种行为似乎是不必要的,而且很奇怪,因为 f(包括 g?)的字节码只构建一次。或者可能只是 g 的默认参数在每次调用 f?

时重新实例化

使用内部函数的现有字节码重建内部函数。使用dis.

很容易看出
>>> import dis
>>> def make_func():
...     def my_func():
...         pass
...     return my_func
>>> dis.dis(make_func.__code__)
  3       0 LOAD_CONST               1 (<code object my_func at [...]", line 3>)
          3 MAKE_FUNCTION            0
          6 STORE_FAST               0 (my_func)

  5       9 LOAD_FAST                0 (my_func)
         12 RETURN_VALUE

现在如果你这样做:

>>> f1 = make_func()
>>> f2 = make_func()
>>> f1 is f2
False
>>> f1.__code__ is f2.__code__
True

第一个误解:"when Python parses the source code of a function, it compiles it to bytecode but doesn't run this bytecode before the function is called (which is why illegal variable names in functions does not throw an exception unless you call the function)." 明确地说,您的误解是"illegal variable names in functions does not throw an exception unless you call the function"。在函数执行之前,不会捕获未分配的名称。

看看这个简单的测试:

In [1]: def g(a):
   ...:     123onetwothree = a
  File "<ipython-input-5-48a83ac30c7b>", line 2
    123onetwothree = a

第二个误解:"default arguments are not instantiated during this initial setup of the function, but only when the function is called for the first time..."。这是不正确的。

In [7]: def f(x=[]):
   ...:     print(x)
   ...:     x.append(1)
   ...:     print(x)
   ...:
   ...:

In [8]: f.__defaults__
Out[8]: ([],)

In [9]: f()
[]
[1]

In [10]: f.__defaults__
Out[10]: ([1],)

In [11]:

对于您的示例,每次您 运行 f 都会重新实例化默认参数 因为您在 f[=35 中定义了 g =].最好的理解方式是将 def 语句视为 function 对象的构造函数,并将默认参数视为此构造函数的参数。每次你 运行 def some_function 就像重新调用构造函数一样,函数被 重新定义 就像在 body 中写了 g = function(a=[])f.

回复评论

In [11]: def f(x=h()): pass
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-11-ecbb4a9f8673> in <module>()
----> 1 def f(x=h()): pass

NameError: name 'h' is not defined

只需查看 fdis 的字节码:

dis(f)
  2           0 BUILD_LIST               0
              3 LOAD_CONST               1 (<code object g at 0x7febd88411e0, file "<ipython-input-21-f2ef9ebb6765>", line 2>)
              6 LOAD_CONST               2 ('f.<locals>.g')
              9 MAKE_FUNCTION            1
             12 STORE_FAST               1 (g)

  6          15 SETUP_LOOP              46 (to 64)
             18 LOAD_GLOBAL              0 (range)
             21 LOAD_FAST                0 (x)
             24 LOAD_FAST                0 (x)
             27 LOAD_CONST               3 (2)
             30 BINARY_ADD
             31 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             34 GET_ITER
        >>   35 FOR_ITER                25 (to 63)
             38 STORE_FAST               2 (y)

(为简洁起见被截断)

g加载的代码对象:

3 LOAD_CONST               1 (<code object g at 0x7febd88411e0, file "<ipython-input-21-f2ef9ebb6765>", line 2>)

不包含任何可变结构,它只包含可执行代码和其他不可变信息。你也可以看一看:

dis(f.__code__.co_consts[1])
  3           0 LOAD_FAST                1 (a)
              3 LOAD_ATTR                0 (append)
              6 LOAD_FAST                0 (y)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 POP_TOP

  4          13 LOAD_FAST                1 (a)
             16 RETURN_VALUE

每次调用 f 时,都会调用 MAKE_FUNCTION,它会根据那里已经存在的字节码重新创建函数。