函数内变量作用域 & 全局/局部变量 & global

Java 不存在全局变量,函数使用的变量只能通过参数传入或函数内定义或方法使用类的成员变量。但是由于 Python 存在全局变量,使得函数内变量的作用域没有那么明确。

下面的例子对于有一定基础的人来说,应该能知道出错的原因。

>>> def f1(a):
...     print(a)
...     print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined
>>> b = 6
>>> f1(3)
3
6

但如果,在函数中修改了 b 的值,如下所示,那么错误的原因可能就令人疑惑。

>>> b = 6
>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

这种情况的原因是:

  1. Python 在编译函数的定义体时,就会明确各个变量的作用域。
  2. Python 不要求声明变量,但是假定如果在函数的定义体内对变量进行了赋值,那么这个变量就是局部变量。
  3. 如果没有在函数的定义体内赋值,如果入参没有,会从全局变量中寻找。

如果在函数赋值时让解释器将 b 变量当作全局变量,需要使用 global 关键字。

>>> b = 6
>>> def f3(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
...
>>> f3(3)
3
6
>>> b
9

通过 dis 模块可以反汇编 Python 函数字节码,如下对比了 f1 和 f2 的字节码内容:

>>> from dis import dis
>>> dis(f1)
 2           0 LOAD_GLOBAL              0 (print)
             3 LOAD_FAST                0 (a)
             6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             9 POP_TOP
 3          10 LOAD_GLOBAL              0 (print)
            13 LOAD_GLOBAL              1 (b)
            16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
            19 POP_TOP
            20 LOAD_CONST               0 (None)
            23 RETURN_VALUE
>>> dis(f2)
 2            0 LOAD_GLOBAL             0 (print)
              3 LOAD_FAST               0 (a)
              6 CALL_FUNCTION           1 (1 positional, 0 keyword pair)
              9 POP_TOP
 3           10 LOAD_GLOBAL             0 (print)
             13 LOAD_FAST               1 (b)
             16 CALL_FUNCTION           1 (1 positional, 0 keyword pair)
             19 POP_TOP
 4           20 LOAD_CONST              1 (9)
             23 STORE_FAST              1 (b)
             26 LOAD_CONST              0 (None)
             29 RETURN_VALUE

函数闭包 & 自由变量 & nonlocal

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。

如下所示,averager 函数就是闭包,series 就是 averager 函数引用、但是不在 averager 函数中引用的非全局变量。

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager
 
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量。

虽然 make_averager 函数在调用 avg(10) 时就已经返回了,而它的本地作用域已经不复返了,但是被 averager 函数引用的 series 变量被保存到了 averager 对象的 __code__ 属性中了。审查 make_averager 创建的函数对象:

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

series 的绑定在返回的 avg 函数的 __closure__ 属性中。avg.__closure__ 中的各个元素对应于 avg.__code__.co_freevars 中的一个名称。这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。

>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

看似自由变量很容易掌握,但是会因为不注意的赋值操作而出错,如下所示:

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count+= 1
        total+= new_value
        return total / count
    return averager
 
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

由于这个例子的 averager 函数对 count 进行了赋值,使得 Python 将 count 认定为局部变量,从而出错。

而在上个例子中,利用了列表是可变对象的特性,通过 append 来修改列表内容,避免了赋值操作,避免了出错。

为了解决这个问题,应该使用 nonlocal 关键词来将变量标记为自由变量。

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count+= 1
        total+= new_value
        return total / count
    return averager