Python global和nonlocal的由来和用法

global

先来看这样一个例子:我们定义并测试了一个函数,它读取两个变量的值:一个是局部变量 a,是函数的参数;另一个是变量 b,这个函数没有定义它。

>>> 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: name 'b' is not defined

出现错误并不奇怪。 如果先给全局变量 b 赋值,然后再调用 f,那就不会出错:

>>> b = 6
>>> f1(3)
3
6

再来看下一个例子: f2 前两行代码与上一个例子的 f1 一样,然后为 b 赋值,再打印它的值。但是,在赋值之前,第二个 print 失败了。

>>> 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

事实上,Python 编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了。Python会尝试从本地环境获取 b。后面调用f2(3)时,f2的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值。

这不是缺陷,而是设计选择:Python不要求声明变量,但是认为在函数定义体中赋值的变量是局部变量。

如果在函数中赋值时想让解释器把 b 当成全局变量,就要使用 global 声明:

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

nonlocal

nonlocal时,先说一下闭包

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

举个栗子吧:

假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值; 例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此 平均值要考虑至目前为止所有的价格。

>>> avg(10) 
10.0 
>>> avg(11) 
10.5 
>>> avg(12) 
11.0

我们可能会这样使用类实现:

class Averager():

    def __init__(self):
        self.series = []

    def __call__(self, new_value): # 实现__call__方法 使该类的对象是可调用对象
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

结果:

>>> avg = Averager() 
>>> avg(10) 
10.0 
>>> avg(11) 
10.5 
>>> avg(12) 
11.0

下面是函数式实现:

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

调用 make_averager 时,返回一个 averager 函数对象。每次调用 averager 时,它会把参数添加到系列值中,然后计算当前平均值.

>>> avg = make_averager() 
>>> avg(10) 
10.0 
>>> avg(11) 
10.5 
>>> avg(12)
11.0

我们知道Averager 类的实例 avg 在哪里存储历史值很明显:就是self.series 实例属性。但是第二个示例中的 avg 函数在哪里寻找 series 呢?

注意seriesmake_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10) 时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。

那为什么还可以调用呢?这时候回头看看闭包的定义应该就会明白定义的意思.

averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量
在这里插入图片描述
审查返回的 averager 对象,我们可以发现 Python__code__ 属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称

>>> 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]

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

注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。

现在说回 nonlocal

前面实现 make_averager 函数的方法效率不高。我们把所有值存储在历史数列中,然后在每次调用 averager 时使用 sum 求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_valuereturn 
        total / count

    return averager

但是我们却会得到如下结果:

>>> avg = make_averager() 
>>> avg(10) 
Traceback (most recent call last):
    ... 
UnboundLocalError: local variable 'count' referenced before assignment

问题出现在这里,当 count 是数字或任何不可变类型时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。

之前的例子没遇到这个问题,因为我们没有给 series 赋值,我们只是调用 series.append,并把它传给 sumlen。也就是说,我们利用了列表是可变的对象这一事实。

但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。 如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量了,因此不会保存在闭包中。

所以 !!!

为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变 量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。

最新版 make_averager 的正确实现如下

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

本文参考书籍:流畅的Python

展开阅读全文
©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值