浅析函数装饰器和闭包(二)

多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运行。为了理解闭包,我们要退后一步,先了解Python中的变量作用域

  • 变量作用域规则

举个例子来说
这里写图片描述
在这个函数中,它读取两个变量的值,一个是局部变量a,是函数的参数;另一个是变量b,这个函数没有定义它
出现错误并不奇怪,如果先给全局变量b赋值,然后再调用f,那就不会出错

接着,看下个例子
这里写图片描述
注意,首先输出了3,这表明print(a)语句执行了。但是第二个语句print(b)执行不了。一开始我很吃惊,我觉得会打印6,因为有个全局变量b,而且是在print(b)之后为局部变量b赋值的

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

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

如果在函数中赋值时想让解释器把b当成全局变量,要使用global声明:
这里写图片描述

  • 闭包

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量

闭包这个概念难以掌握,我们通过示例理解

假如有个名为avg的函数,它的作用是计算不断增加的系列值的均值;Averager的实例是可调用对象
这里写图片描述

接着是函数式实现,使用高阶函数make_averager
这里写图片描述
注意,这两个示例有共通之处:调用Averager()或make_averager()得到一个可调用对象avg,它会更新历史值,然后计算当前均值。在第一个示例中,avg是Averager的实例;在第二个示例中是内部函数averager。不管怎样,我们都只需调用avg(n),把n放入系列值,然后重新计算均值。

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

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

在averager函数中,series是自由变量(free variable),指未在本地作用域中绑定的变量
这里写图片描述

averager的闭包延伸到那个函数的作用域之外,包含自由变量series的绑定

审查返回的averager对象,我们发现Python在__code__属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称
这里写图片描述

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

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

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

猜你喜欢

转载自blog.csdn.net/Jonms/article/details/80889186