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

Instantiation time for mutable default arguments of closures in Python

我的理解是,当python解析函数的源代码时,它将其编译为字节码,但在调用函数之前不运行此字节码(这就是为什么函数中的非法变量名不会引发异常,除非调用该函数)。

在函数的初始设置过程中,不会实例化默认参数,但只有在第一次调用函数时才会实例化默认参数,无论是否提供了参数。默认参数的这个相同实例用于所有将来的调用,可以通过使用可变类型作为默认参数来查看。

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

1
2
3
4
5
6
7
8
9
10
11
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))

这打印出来了

1
2
3
4
5
6
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很容易看到。

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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

如果你这样做:

1
2
3
4
5
6
>>> f1 = make_func()
>>> f2 = make_func()
>>> f1 is f2
False
>>> f1.__code__ is f2.__code__
True

第一个误解是:"当python解析函数的源代码时,它将其编译为字节码,但在调用函数之前不运行这个字节码(这就是为什么函数中的非法变量名不会引发异常,除非调用函数)。"要清楚,您的误解是"函数中的非法变量名除非调用函数,否则不会引发异常"。在执行函数之前,不会捕获未分配的名称。

看看这个简单的测试:

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

第二个误解是:"在函数的初始设置过程中,不会实例化默认参数,但仅当函数第一次被调用时……"。这是不正确的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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中定义了g,默认参数都会重新激活。最好的方法是将def语句视为function对象的构造函数,以及默认参数(如该构造函数的参数)。每次运行def some_function时,就像重新调用构造函数一样,重新定义函数,就像在f的主体中写入了g = function(a=[])一样。

回应评论

1
2
3
4
5
6
7
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的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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加载的代码对象:

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

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

1
2
3
4
5
6
7
8
9
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,它根据已经存在的字节代码重新创建函数。