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 ...
随机推荐
- @topcoder - SRM603D1L3@ SumOfArrays
目录 @desription@ @solution@ @accepted code@ @details@ @desription@ 给定两个长度为 n 的数列 A, B.现你可以将两数列重排列,然后对 ...
- 【从单体架构到分布式架构】(三)请求增多,单点变集群(2):Nginx
上一个章节,我们学习了负载均衡的理论知识,那么是不是把应用部署多套,前面挂一个负载均衡的软件或硬件就可以应对高并发了?其实还有很多问题需要考虑.比如: 1. 当一台服务器挂掉,请求如何转发到其他正常的 ...
- Dart Memo for Android Developers
Dart Memo for Android Developers Dart语言一些语法特点和编程规范. 本文适合: 日常使用Kotlin, 突然想写个Flutter程序的Android程序员. Dar ...
- Spark GraphX从入门到实战
第1章 Spark GraphX 概述 1.1 什么是 Spark GraphX Spark GraphX 是一个分布式图处理框架,它是基于 Spark 平台提供对图计算和图挖掘简洁易用的而丰 ...
- (五)POI-设置单元格的对齐方式
原文链接:https://blog.csdn.net/class157/article/details/92817149 package com.java.poi; import org.apache ...
- 附024.Kubernetes_v1.18.3高可用部署架构二
kubeadm介绍 kubeadm概述 参考<附003.Kubeadm部署Kubernetes>. kubeadm功能 参考<附003.Kubeadm部署Kubernetes> ...
- vue+ajax的实现
html <tr> <td>用户名</td> <td id="t01"><input type="text" ...
- selenium(3)-针对鼠标的操作
背景 用selenium做自动化,有时候会遇到需要模拟鼠标操作才能进行的情况,比如单击.双击.点击鼠标右键.拖拽等等. 而selenium给我们提供了一个类来处理这类事件-----------Acti ...
- WeChair项目Alpha冲刺(2/10)
团队项目进行情况 1.昨日进展 Alpha冲刺第二天 昨日进展: 前端完成小程序首页的html+css设计 后端springboot项目搭建完成 详情参见github 数据库也初步建成一些表格, ...
- 深入浅出PyTorch(算子篇)
Tensor 自从张量(Tensor)计算这个概念出现后,神经网络的算法就可以看作是一系列的张量计算.所谓的张量,它原本是个数学概念,表示各种向量或者数值之间的关系.PyTorch的张量(torch. ...