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
?)的字节码只能构建一次。 或者,它可能只是每次调用f
重新实例化的默认参数g
?
第一个误解是:“当Python解析函数的源代码时,它会将其编译为字节码,但在函数被调用之前不会运行此字节码(这就是为什么函数中的非法变量名称除非调用函数才会抛出异常)“。 清楚的是,你的误解是“除非你调用函数,否则函数中的非法变量名不会抛出异常”。 在执行该功能之前,未分配的名称将不会被捕获。
看看这个简单的测试:
In [1]: def g(a):
...: 123onetwothree = a
File "<ipython-input-5-48a83ac30c7b>", line 2
123onetwothree = a
第二种误解:“在函数的初始设置过程中,默认参数没有被实例化,但仅在第一次调用该函数时...”。 这是不正确的。
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
对象的构造function
,并将默认参数(如参数)视为此构造函数。 每次运行def some_function
,就像再次调用构造函数一样,函数被重新定义,就像在f
的主体中写入g = function(a=[])
。
回应评论
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
内部函数使用内部函数的现有字节码进行重建。 使用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
用dis
看看f
的字节码:
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
,它会根据已存在的字节代码重新创建函数。
上一篇: Instantiation time for mutable default arguments of closures in Python
下一篇: Strange behavior with a list as a default function argument