python 装饰器(二):装饰器基础(二)变量作用域规则,闭包,nonlocal声明
变量作用域规则
在示例 7-4 中,我们定义并测试了一个函数,它读取两个变量的值:一个是局部变量 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: global name 'b' is not defined
出现错误并不奇怪。 在示例 7-4 中,如果先给全局变量 b 赋值,然后再调用 f,那就不会出错:
>>> b = 6
>>> f1(3)
3
6
下面看一个可能会让你吃惊的示例。
看一下示例 7-5 中的 f2 函数。前两行代码与示例 7-4 中的 f1 一样,然后为 b 赋值,再打印它的值。可是,在赋值之前,第二个 print 失败了。
示例 7-5 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
>>>
闭包
其实,闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
起初,avg 是这样使用的:
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
avg 从何而来,它又在哪里保存历史值呢?
初学者可能会像示例 7-8 那样使用类实现。
示例 7-8 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
示例 7-9 是函数式实现,使用高阶函数 make_averager。
示例 7-9 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 时,它会把参数添加到系列值中,然后计算当前平均值,如
示例 7-10 所示。
示例 7-10 测试示例 7-9
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
注意,这两个示例有共通之处:调用 Averager() 或make_averager() 得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。
在示例 7-8 中,avg 是 Averager 的实例;在示例 7-9中是内部函数 averager。不管怎样,我们都只需调用 avg(n),把 n放入系列值中,然后重新计算均值。
Averager 类的实例 avg 在哪里存储历史值很明显:self.series 实例属性。但是第二个示例中的 avg 函数在哪里寻找 series 呢?
注意,series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10)时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。
在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量,参见图 7-1。
图 7-1:averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定
审查返回的 averager 对象,我们发现 Python 在 __code__ 属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称,如示例 7-11
所示。示例 7-11 审查 make_averager(见示例 7-9)创建的函数
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
series 的绑定在返回的 avg 函数的 __closure__ 属性中。avg.__closure__ 中的各个元素对应于avg.__code__.co_freevars 中的一个名称。
这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。这些属性的值如示例 7-12 所示。
示例 7-12 接续示例 7-11
>>> 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 函数的方法效率不高。在示例 7-9 中,我们把所有值存储在历史数列中,然后在每次调用 averager 时使用 sum 求和。
更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。
示例 7-13 计算移动平均值的高阶函数,不保存所有历史值,但有缺陷
def make_averager():
count = 0
total = 0 def averager(new_value):
count += 1
total += new_value
return total / count return averager
尝试使用示例 7-13 中定义的函数,会得到如下结果:
>>> 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 变量也受这个问题影响。
但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。
这样,count 就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。
如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。最新版 make_averager 的正确实现如示例 7-14 所示。
示例 7-14 计算移动平均值,不保存所有历史(使用 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 装饰器(二):装饰器基础(二)变量作用域规则,闭包,nonlocal声明的更多相关文章
- Python 变量作用域,闭包和装饰器
from dis import dis b = 6 def f1(a): print(a)print(b) b = 9 f1(3) print(dis(f1)) # dis模块可以查看python函数 ...
- Python--高阶函数、函数嵌套、名称空间及变量作用域、闭包、装饰器
1.高阶函数(map/reduce/filter) 高阶函数是指函数的参数可以是函数 这篇总结几个常用的高阶函数:map/reduce/filter map函数.reduce函数.filter函数都是 ...
- 4、TensorFlow基础(二)常用API与变量作用域
1.图.操作和张量 TensorFlow 的计算表现为数据流图,所以 tf.Graph 类中包含一系列表示计算的操作对象(tf.Operation),以及在操作之间流动的数据 — 张量对象(tf.Te ...
- Python基础:11变量作用域和闭包
一:变量作用域 变量可以是局部域或者全局域.定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域. 全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数, ...
- Python全栈工程师(函数嵌套、变量作用域)
ParisGabriel 感谢 大家的支持 每天坚持 一天一篇 点个订阅 ...
- python 变量作用域、闭包
先看一个问题: 下面代码输出的结果是0,换句话说,这个fucn2虽然已经用global声明了variable1,但还是没有改变变量的值 def func1(): variable1=0 def fun ...
- python学习列表(Lists).基础二
列表(Lists) 序列是Python中最基本的数据结构,序列中的每个元素都分配一个数字,它的第一个索引是0第二个索引是1,依次类推. 列表是最常用的Python数据类型,它可以作为一个方括号内的逗号 ...
- Python基础之变量作用域
一.分类: 二.变量名的查找规则: 三.局部变量: 四.全局变量: 五.global语句: 六.nonlocal语句: 七.基础代码: # 全局变量:当前.py文件内部都可访问 g01 = 100 d ...
- Python入门笔记(22):Python函数(5):变量作用域与闭包
一.全局变量与局部变量 一个模块中,最高级别的变量有全局作用域. 全局变量一个特征就是:除非被删除,否则他们存活到脚本运行结束,且对于所有的函数都可访问. 当搜索一个标识符(也称变量.名字等),Pyt ...
随机推荐
- Linux防火墙iptables详解
iptables详解(思维导图) 1. 概述 1.1 iptable简介 1.2 防火墙的种类 1.3 netfilter 2. iptables的工作流程 2.1 iptables工作图示 2.2 ...
- 软件包,API,SDK的区别
参考资料: https://www.jianshu.com/p/cac186cb168b https://blog.csdn.net/snowin1994/article/details/806080 ...
- vulstack红队评估(五)
一.环境搭建: ①根据作者公开的靶机信息整理 虚拟机密码: Win7: heart 123.com #本地管理员用户 sun\Administrator dc123.com #域管用户,改 ...
- JVM之HotSpot虚拟机是如何发起内存回收的?
1.背景 在上一节中,我们掌握了垃圾收集的一些算法,也弄明白了分代回收的原理, 那么HotSpot虚拟机是如何发起内存回收的? 2.如何找到GC Roots根节点(枚举根节点) 从可达性分析中GC R ...
- MyBatis一对多嵌套list返回结果集以及分页查询问题处理
这两天在整理原有系统接口时,遇到后端的人员-角色-菜单的权限接口没有进行连表的关联查询操作,前端拿数据非常不方便,现在将接口相关sql进行修改并让前端可以一次性拿到想要的数据 原有的单表简单sql: ...
- mysql语句基本练习
select ename,job from emp where job in ('MANAGER','ANALYET','SALESMAN') 1.查询出工作岗位为MANAGER.ANALYST.SA ...
- 入门大数据---Spark_Transformation和Action算子
一.Transformation spark 常用的 Transformation 算子如下表: Transformation 算子 Meaning(含义) map(func) 对原 RDD 中每个元 ...
- Python实用笔记 (8)高级特性——迭代
如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration). 比如dict就可以迭代: >>> d = {'a ...
- Srapy 爬取知乎用户信息
今天用scrapy框架爬取一下所有知乎用户的信息.道理很简单,找一个知乎大V(就是粉丝和关注量都很多的那种),找到他的粉丝和他关注的人的信息,然后分别再找这些人的粉丝和关注的人的信息,层层递进,这样下 ...
- 写给程序员的机器学习入门 (七) - 双向递归模型 (BRNN) - 根据上下文补全单词
这一篇将会介绍什么是双向递归模型和如何使用双向递归模型实现根据上下文补全句子中的单词. 双向递归模型 到这里为止我们看到的例子都是按原有顺序把输入传给递归模型的,例如传递第一天股价会返回根据第一天股价 ...