1. 变量作用域规则

在示例1-1 中,我们定义并测试了一个函数,它读取两个变量的值:一个是局部变量a,是函数的参数;另一个是变量b,这个函数没有定义它。

示例1-1 一个函数,读取一个局部变量和一个全局变量

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

出现错误并不奇怪。3 在示例1-1 中,如果先给全局变量b 赋值,然后再调用f1,那就不会出错:

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

下面看一个可能会让你吃惊的示例。看一下示例1-2 中的f2 函数。前两行代码与示例1-1 中的f1 一样,然后为b 赋值,再打印它的值。可是,在赋值之前,第二个print 失败了。

示例1-2 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

注意,首先输出了3,这表明print(a) 语句执行了。但是第二个语句print(b) 执行不了。一开始我很吃惊,我觉得会打印6,因为有个全局变量b,而且是在print(b) 之后为局部变量b 赋值的。可事实是,Python 编译函数的定义体时,它判断b 是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取b。后面调用f2(3) 时,f2 的定义体会获取并打印局部变量a 的值,但是尝试获取局部变量b 的值时,发现b 没有绑定值。

这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。这比JavaScript 的行为好多了,JavaScript 也不要求声明变量,但是如果忘记把变量声明为局部变量(使用var),可能会在不知情的情况下获取全局变量。

如果在函数中赋值时想让解释器把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
>>>

了解Python 的变量作用域之后,接下来可以讨论闭包了。

2. 闭包

人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。因此,很多人是同时知道这两个概念的。其实,闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

这个概念难以掌握,最好通过示例理解。假如有个名为avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。起初,avg 是这样使用的:

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

avg 从何而来,它又在哪里保存历史值呢?初学者可能会像示例2-1 那样使用类实现。

示例2-1 average_oo.py:计算移动平均值的类

class Averager():
def __init__(self):
    self.series = []
def __call__(self, new_value):
  self.series.append(new_value)
  total = sum(self.series)
  return total/len(self.series)

Averager 的实例是可调用对象:

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

示例2-2 是函数式实现,使用高阶函数make_averager。

示例2-2 average.py:计算移动平均值的高阶函数

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 时,它会把参数添加到系列值中,然后计算当前平均值,如示例2-3 所示。

示例2-3 测试示例2-2:

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

注意,这两个示例有共通之处:调用Averager() 或make_averager() 得到一个可调用对象avg,它会更新历史值,然后计算当前均值。在示例2-1 中,avg 是Averager 的实例;在示例2-2 中是内部函数averager。不管怎样,我们都只需调用avg(n),把n 放入系列值中,然后重新计算均值。

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

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

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

 

审查返回的averager 对象,我们发现Python 在code 属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称,如示例2-4 所示。

示例2-4 审查make_averager(见示例2-3)创建的函数

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

series 的绑定在返回的avg 函数的closure 属性中。avg.closure 中的各个元素对应于avg.code.co_freevars 中的一个名称。这些元素是cell 对象,有个cell_contents 属性,保存着真正的值。这些属性的值如示例2-5 所示。

示例2-5 接续示例2-4   
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。

3. nonlocal声明

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

示例3-1 计算移动平均值的高阶函数,不保存所有历史值,但有缺陷

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

尝试使用示例3-1 中定义的函数,会得到如下结果:

>>> 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 变量也受这个问题影响。示例2-2 没遇到这个问题,因为我们没有给series 赋值,我们只是调用series.append,并把它传给sum 和len。也就是说,我们利用了列表是可变的对象这一事实。但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count = count + 1,其实会隐式创建局部变量count。这样,count 就不是自由变量了,因此不会保存在闭包中。

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

示例3-2 计算移动平均值,不保存所有历史(使用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

写在最后:至此,我们了解了Python 闭包,下面可以使用嵌套函数正式实现装饰器

物联沃分享整理
物联沃-IOTWORD物联网 » python中的闭包

发表评论