变量作用域规则

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

  1. >>> def f1(a):
  2. ... print(a)
  3. ... print(b)
  4. ...
  5. >>> f1(3)
  6. 3
  7. Traceback (most recent call last):
  8. File "<stdin>", line 1, in <module>
  9. File "<stdin>", line 3, in f1
  10. NameError: global name 'b' is not defined

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

  1. >>> b = 6
  2. >>> f1(3)
  3. 3
  4. 6

下面看一个可能会让你吃惊的示例。
看一下示例 7-5 中的 f2 函数。前两行代码与示例 7-4 中的 f1 一样,然后为 b 赋值,再打印它的值。可是,在赋值之前,第二个 print 失败了。
示例 7-5 b 是局部变量,因为在函数的定义体中给它赋值了

  1. >>> b = 6
  2. >>> def f2(a):
  3. ... print(a)
  4. ... print(b)
  5. ... b = 9
  6. ...
  7. >>> f2(3)
  8. 3
  9. Traceback (most recent call last):
  10. File "<stdin>", line 1, in <module>
  11. File "<stdin>", line 3, in f2
  12. 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 声明:

  1. >>> b = 6
  2. >>> def f3(a):
  3. ... global b
  4. ... print(a)
  5. ... print(b)
  6. ... b = 9
  7. ...
  8.  
  9. >>> f3(3)
  10. 3
  11. 6
  12. >>> b
  13. 9
  14.  
  15. >>> f3(3)
  16. 3
  17. 9
  18. >>> b = 30
  19. >>> b
  20. 30
  21. >>>

闭包

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

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

  1. >>> avg(10)
  2. 10.0
  3. >>> avg(11)
  4. 10.5
  5. >>> avg(12)
  6. 11.0

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

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

  1. class Averager():
  2.  
  3. def __init__(self):
  4. self.series = []
  5.  
  6. def __call__(self, new_value):
  7. self.series.append(new_value)
  8. total = sum(self.series)
  9. return total/len(self.series)

Averager 的实例是可调用对象:

  1. >>> avg = Averager()
  2. >>> avg(10)
  3. 10.0
  4. >>> avg(11)
  5. 10.5
  6. >>> avg(12)
  7. 11.0

示例 7-9 是函数式实现,使用高阶函数 make_averager。
示例 7-9 average.py:计算移动平均值的高阶函数

  1. def make_averager():
  2. series = []
  3.  
  4. def averager(new_value):
  5. series.append(new_value)
  6. total = sum(series)
  7. return total/len(series)
  8.  
  9. return averager

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

示例 7-10 测试示例 7-9

  1. >>> avg = make_averager()
  2. >>> avg(10)
  3. 10.0
  4. >>> avg(11)
  5. 10.5
  6. >>> avg(12)
  7. 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)创建的函数

  1. >>> avg.__code__.co_varnames
  2. ('new_value', 'total')
  3. >>> avg.__code__.co_freevars
  4. ('series',)

series 的绑定在返回的 avg 函数的 __closure__ 属性中。avg.__closure__ 中的各个元素对应于avg.__code__.co_freevars 中的一个名称。

这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。这些属性的值如示例 7-12 所示。
示例 7-12 接续示例 7-11

  1. >>> avg.__code__.co_freevars
  2. ('series',)
  3. >>> avg.__closure__
  4. (<cell at 0x107a44f78: list object at 0x107a91a48>,)
  5. >>> avg.__closure__[0].cell_contents
  6. [10, 11, 12]

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

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

nonlocal声明

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

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

  1. def make_averager():
  2. count = 0
  3. total = 0
  4.  
  5. def averager(new_value):
  6. count += 1
  7. total += new_value
  8. return total / count
  9.  
  10. return averager

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

  1. >>> avg = make_averager()
  2. >>> avg(10)
  3. Traceback (most recent call last):
  4. ...
  5. UnboundLocalError: local variable 'count' referenced before assignment
  6. >>>

问题是,当 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 修正)

  1. def make_averager():
  2. count = 0
  3. total = 0
  4.  
  5. def averager(new_value):
  6. nonlocal count, total
  7. count += 1
  8. total += new_value
  9. return total / count
  10.  
  11. return averager

python 装饰器(二):装饰器基础(二)变量作用域规则,闭包,nonlocal声明的更多相关文章

  1. Python 变量作用域,闭包和装饰器

    from dis import dis b = 6 def f1(a): print(a)print(b) b = 9 f1(3) print(dis(f1)) # dis模块可以查看python函数 ...

  2. Python--高阶函数、函数嵌套、名称空间及变量作用域、闭包、装饰器

    1.高阶函数(map/reduce/filter) 高阶函数是指函数的参数可以是函数 这篇总结几个常用的高阶函数:map/reduce/filter map函数.reduce函数.filter函数都是 ...

  3. 4、TensorFlow基础(二)常用API与变量作用域

    1.图.操作和张量 TensorFlow 的计算表现为数据流图,所以 tf.Graph 类中包含一系列表示计算的操作对象(tf.Operation),以及在操作之间流动的数据 — 张量对象(tf.Te ...

  4. Python基础:11变量作用域和闭包

    一:变量作用域 变量可以是局部域或者全局域.定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域. 全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数, ...

  5. Python全栈工程师(函数嵌套、变量作用域)

    ParisGabriel   感谢 大家的支持                                                               每天坚持 一天一篇 点个订阅 ...

  6. python 变量作用域、闭包

    先看一个问题: 下面代码输出的结果是0,换句话说,这个fucn2虽然已经用global声明了variable1,但还是没有改变变量的值 def func1(): variable1=0 def fun ...

  7. python学习列表(Lists).基础二

    列表(Lists) 序列是Python中最基本的数据结构,序列中的每个元素都分配一个数字,它的第一个索引是0第二个索引是1,依次类推. 列表是最常用的Python数据类型,它可以作为一个方括号内的逗号 ...

  8. Python基础之变量作用域

    一.分类: 二.变量名的查找规则: 三.局部变量: 四.全局变量: 五.global语句: 六.nonlocal语句: 七.基础代码: # 全局变量:当前.py文件内部都可访问 g01 = 100 d ...

  9. Python入门笔记(22):Python函数(5):变量作用域与闭包

    一.全局变量与局部变量 一个模块中,最高级别的变量有全局作用域. 全局变量一个特征就是:除非被删除,否则他们存活到脚本运行结束,且对于所有的函数都可访问. 当搜索一个标识符(也称变量.名字等),Pyt ...

随机推荐

  1. @codeforces - 549E@ Sasha Circle

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 给定两个点集 M 与 S,求是否存在一个圆能够分割两个点集. 原 ...

  2. TensorFlow从0到1之TensorBoard可视化数据流图(8)

    TensorFlow 使用 TensorBoard 来提供计算图形的图形图像.这使得理解.调试和优化复杂的神经网络程序变得很方便.TensorBoard 也可以提供有关网络执行的量化指标.它读取 Te ...

  3. 学员操作——制作5s关闭广告

    <!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>广 ...

  4. Scanner扫描器的使用

    Scanner:扫描器,可以通过Scanner类扫描用户在控制台录入的数据. 1.导包 //导包快捷键Alt+Enter 2.创建键盘录入对象 //键盘录入对象的名称为 “sc” 3.接收数据 //将 ...

  5. Spring AOP学习笔记04:AOP核心实现之创建代理

    上文中,我们分析了对所有增强器的获取以及获取匹配的增强器,在本文中我们就来分析一下Spring AOP中另一部分核心逻辑--代理的创建.这部分逻辑的入口是在wrapIfNecessary()方法中紧接 ...

  6. Ubuntu k80深度学习环境搭建

    英伟达驱动安装 英伟达驱动下载:https://www.nvidia.cn/Download/driverResults.aspx/135493/cn/ 由于是驱动的冲突,那么自然是要杀掉和显卡结合不 ...

  7. Python实用笔记 (5)使用dictionary和set

    dictionary 通过键值存储,具有极快的查找速度,但占用空间比list大很多 举个例子,假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list: names = ['Micha ...

  8. Java中List集合去除重复数据的方法1

    1. 循环list中的所有元素然后删除重复 public   static   List  removeDuplicate(List list)  {         for  ( int  i  = ...

  9. 序列推荐(transformer)

    目录 Attention演进(RNN&LSTM&GRU&Seq2Seq + Attention机制) LSTM GRU Seq2Seq + Attention机制 Attent ...

  10. jQuery制作div板块拖动层排序

    html结构: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www ...