目录

  1. 问题
  2. 猜想与证明
  3. 总结
  4. Reference
  5. END

问题

最近在写一个工作上的代码时,遇到要使用嵌套函数的情况,但是总是报一个 UnboundLocalError 的错误。我把问题代码抽象出来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def outer():
outer_list = []
outer_int = 0

def inner():
print('INNER')
print(f"outer_list={outer_list}")
print(f"outer_int={outer_int}")
outer_list.append(1)
outer_int = 1

inner()
print('OUTER')
print(f"outer_list={outer_list}")
print(f"outer_int={outer_int}")

outer()

运行该代码会报如下 UnboundLocalError 错误,显示 outer_int 未定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
INNER
outer_list=[]
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-7-0c4860f923cf> in <module>()
16
17
---> 18 outer()

1 frames
<ipython-input-7-0c4860f923cf> in outer()
10 outer_int = 1
11
---> 12 inner()
13 print('OUTER')
14 print(f"outer_list={outer_list}")

<ipython-input-7-0c4860f923cf> in inner()
6 print('INNER')
7 print(f"outer_list={outer_list}")
----> 8 print(f"outer_int={outer_int}")
9 outer_list.append(1)
10 outer_int = 1

UnboundLocalError: local variable 'outer_int' referenced before assignment

outer_listouter_int 同样在 outer() 里进行了定义,按理说在嵌套函数里都可以获取到,但是现在表明只有 outer_list 可以获取到。

但是如果你将第 10 行 outer_int = 1 注释掉,你会发现又不报错了,又可以访问到 outer_int 了:

1
2
3
4
5
6
INNER
outer_list=[]
outer_int=0
OUTER
outer_list=[1]
outer_int=0

这是怎么回事?

猜想与证明

首先表明,这是 feature 而不是 bug!

然后我们需要先引入几个术语:

  • 自由变量 free variable。自由变量一般是相对于嵌套函数来说的。一个嵌套函数的自由变量是指该函数用到了该变量,但其定义并不在该函数内的非全局变量。在上面的代码中, outer_listouter_int 都是 inner() 的自由变量,它们都在 inner() 中被用到,但是定义却是在 outer() 函数中,而且它们也很明显不是 global 的。“自由”意味着它们可以在不同函数之间”自由穿梭“。此外用 nonlocal 声明的变量也是自由变量。
  • Cell 变量 Cell variable(我不太确定中文叫法,暂且就叫 cell 变量吧)。单元变量其实本质上是一种局部变量,是同一变量在不同视角下的说法,该变量在外部函数中定义,但是在内部函数被引用。在上面的代码中, outer_listouter_int 都是 outer() 的 cell 变量,但对 inner() 来说,它们同时也是其下的自由变量。

由上面的现象我们可以猜想,可能是由于 inner() 函数中对 outer_int 的重新赋值导致其从自由变量变为 inner() 函数的局部变量,然后引发 UnboundLocalError

那如何证明我们的猜想呢?

这就又要引入一个概念叫字节码 bytecode。Python 代码执行流程是先将你写的代码编译为一种中间代码,然后运行时再由解释器解释这些中间代码为机器代码来执行。这种中间代码就叫字节码。

如果你观察过运行 Python 文件后的目录变化情况,你会发现在你第一次运行完一个 Python 文件后,当前目录会生成一个 __pycache__ 目录,里面存放着和你运行的 Python 文件同名的文件,只不过后缀是 .pyc ,这些文件里存放的就是字节码。

这些字节码里存放着编译后的各种底层操作,我们可以从这些字节码里看到详细的操作细节。但是字节码是二进制文件,我们需要使用内置的 dis 模块来帮助我们反汇编(disassembly)这些字节码,生成格式化后的、人类阅读友好的字节码指令。

为方便我们后面讨论,我先将两个关键的字节码指令及其意义列出如下:

  • LOAD_FAST :将指向局部对象 co_varnames[var_num] 的引用推入栈顶。对应于局部变量。
  • LOAD_GLOBAL :加载名称为 co_names[namei] 的全局对象推入栈顶。对应于全局变量。
  • LOAD_DEREF :加载包含在 cell 的第 i 个空位中的单元并释放可用的存储空间。将一个 cell 所包含对象的引用推入栈顶。对应于自由变量和 cell 变量。

相应的还有 STORE_* 的指令,其意义和 LOAD_* 相反,我就不赘述了。

OK,我们现在来看下注释前代码的字节码(Python 3.7.12,下同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
  2           0 BUILD_LIST               0
2 STORE_DEREF 0 (outer_list)

3 4 LOAD_CONST 1 (0)
6 STORE_FAST 0 (outer_int)

5 8 LOAD_CLOSURE 0 (outer_list)
10 BUILD_TUPLE 1
12 LOAD_CONST 2 (<code object inner at 0x7f9f518cdf60, file "<ipython-input-1-0c4860f923cf>", line 5>)
14 LOAD_CONST 3 ('outer.<locals>.inner')
16 MAKE_FUNCTION 8
18 STORE_FAST 1 (inner)

12 20 LOAD_FAST 1 (inner)
22 CALL_FUNCTION 0
24 POP_TOP

13 26 LOAD_GLOBAL 0 (print)
28 LOAD_CONST 4 ('OUTER')
30 CALL_FUNCTION 1
32 POP_TOP

14 34 LOAD_GLOBAL 0 (print)
36 LOAD_CONST 5 ('outer_list=')
38 LOAD_DEREF 0 (outer_list)
40 FORMAT_VALUE 0
42 BUILD_STRING 2
44 CALL_FUNCTION 1
46 POP_TOP

15 48 LOAD_GLOBAL 0 (print)
50 LOAD_CONST 6 ('outer_int=')
52 LOAD_FAST 0 (outer_int)
54 FORMAT_VALUE 0
56 BUILD_STRING 2
58 CALL_FUNCTION 1
60 POP_TOP
62 LOAD_CONST 0 (None)
64 RETURN_VALUE

Disassembly of <code object inner at 0x7f9f518cdf60, file "<ipython-input-1-0c4860f923cf>", line 5>:
6 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('INNER')
4 CALL_FUNCTION 1
6 POP_TOP

7 8 LOAD_GLOBAL 0 (print)
10 LOAD_CONST 2 ('outer_list=')
12 LOAD_DEREF 0 (outer_list) <---- HERE
14 FORMAT_VALUE 0
16 BUILD_STRING 2
18 CALL_FUNCTION 1
20 POP_TOP

8 22 LOAD_GLOBAL 0 (print)
24 LOAD_CONST 3 ('outer_int=')
26 LOAD_FAST 0 (outer_int) <---- HERE
28 FORMAT_VALUE 0
30 BUILD_STRING 2
32 CALL_FUNCTION 1
34 POP_TOP

9 36 LOAD_DEREF 0 (outer_list)
38 LOAD_METHOD 1 (append)
40 LOAD_CONST 4 (1)
42 CALL_METHOD 1
44 POP_TOP

10 46 LOAD_CONST 4 (1)
48 STORE_FAST 0 (outer_int)
50 LOAD_CONST 0 (None)
52 RETURN_VALUE

注意我标记 <---- HERE 的那两行,即第 7 行和第 8 行所对应的字节码。

我们可以看到, outer_list 是一个自由变量,其相关的指令都是 *_DEREF ,第 7 行使用 LOAD_DEREF 来加载 outer_list 。而 outer_int 是一个 inner() 的局部变量,其相关的指令都是 *_FAST ,第 8 行使用 LOAD_FAST 来加载 outer_int 。但是 outer_intinner() 中并未定义,所以会引发 UnboundLocalError

我们再来看下注释后的字节码指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
  2           0 BUILD_LIST               0
2 STORE_DEREF 1 (outer_list) <---- HERE

3 4 LOAD_CONST 1 (0)
6 STORE_DEREF 0 (outer_int) <---- HERE

5 8 LOAD_CLOSURE 0 (outer_int)
10 LOAD_CLOSURE 1 (outer_list)
12 BUILD_TUPLE 2
14 LOAD_CONST 2 (<code object inner at 0x7fa67ec76ed0, file "<ipython-input-1-b646af7e4a0b>", line 5>)
16 LOAD_CONST 3 ('outer.<locals>.inner')
18 MAKE_FUNCTION 8
20 STORE_FAST 0 (inner)

12 22 LOAD_FAST 0 (inner)
24 CALL_FUNCTION 0
26 POP_TOP

13 28 LOAD_GLOBAL 0 (print)
30 LOAD_CONST 4 ('OUTER')
32 CALL_FUNCTION 1
34 POP_TOP

14 36 LOAD_GLOBAL 0 (print)
38 LOAD_CONST 5 ('outer_list=')
40 LOAD_DEREF 1 (outer_list)
42 FORMAT_VALUE 0
44 BUILD_STRING 2
46 CALL_FUNCTION 1
48 POP_TOP

15 50 LOAD_GLOBAL 0 (print)
52 LOAD_CONST 6 ('outer_int=')
54 LOAD_DEREF 0 (outer_int)
56 FORMAT_VALUE 0
58 BUILD_STRING 2
60 CALL_FUNCTION 1
62 POP_TOP
64 LOAD_CONST 0 (None)
66 RETURN_VALUE

Disassembly of <code object inner at 0x7fa67ec76ed0, file "<ipython-input-1-b646af7e4a0b>", line 5>:
6 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('INNER')
4 CALL_FUNCTION 1
6 POP_TOP

7 8 LOAD_GLOBAL 0 (print)
10 LOAD_CONST 2 ('outer_list=')
12 LOAD_DEREF 1 (outer_list)
14 FORMAT_VALUE 0
16 BUILD_STRING 2
18 CALL_FUNCTION 1
20 POP_TOP

8 22 LOAD_GLOBAL 0 (print)
24 LOAD_CONST 3 ('outer_int=')
26 LOAD_DEREF 0 (outer_int) <---- HERE
28 FORMAT_VALUE 0
30 BUILD_STRING 2
32 CALL_FUNCTION 1
34 POP_TOP

9 36 LOAD_DEREF 1 (outer_list)
38 LOAD_METHOD 1 (append)
40 LOAD_CONST 4 (1)
42 CALL_METHOD 1
44 POP_TOP
46 LOAD_CONST 0 (None)
48 RETURN_VALUE

我们可以看到 outer_listouter_int 现在都是自由变量了,自然也不会引发 UnboundLocalError 了。

那么为什么 outer_list 可以在 inner() 中引用并改变呢?其实 outer_list 并没有被改变,其 id 没有变,只是增加了一个值,其内存中的地址并没有变(变量的地址指的是起始地址)。也就是说, outer_list 是一个可变对象,而 outer_int 是一个不可变对象。正是由于不可变对象的这个特性, inner() 中对 outer_int 的重新赋值导致编译器认为其是一个局部变量,而在前面 print 的时候还没定义,自然引发了错误,这也同时可以避免你对一个不可变对象的误操作。如果你将 print 去掉,程序也不会报错,这就相当于创建了一个新对象。

总结

所以,如果你想要在嵌套函数中使用外部函数的不可变对象并想要对其改变,例如 int 对象的 += 操作,则要么使用可变对象替换之,要么在嵌套函数中使用 nonlocal 声明之,使之变成一个自由变量。

Reference

END