变量作用域规则

在示例 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声明的更多相关文章

  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. Dos命令提示符下 - 用sqlcmd执行*.sql语句

    Dos命令提示符下 - 用sqlcmd执行*.sql语句 1)在Dos命令下执行sqlcmd命令(当然事先需要将sqlcmd增加到环境变量中去), 2)下面白色部分替换为服务器名或计算机名即可sqlc ...

  2. TensorFlow从0到1之浅谈深度学习(10)

    DNN(深度神经网络算法)现在是AI社区的流行词.最近,DNN 在许多数据科学竞赛/Kaggle 竞赛中获得了多次冠军. 自从 1962 年 Rosenblat 提出感知机(Perceptron)以来 ...

  3. springboot整合Mybatis(有xml)

    pom.xml <!-- mybatis 支持 SpringBoot --> <dependency> <groupId>org.mybatis.spring.bo ...

  4. pip环境变量配置

    找到python安装目录,进入C:\Users\EDZ\AppData\Local\Programs\Python\Python37-32\Scripts  下.添加此地址到path中 打开cmd 输 ...

  5. 前后端分离项目 nginx配置实践

    新项目采用前后端分离的方式开发,前后端代码打算分开部署(同机器且同域名),但打算支持后端依然可访问静态资源. 搜索nginx配置大部分都通过url前缀进行转发来做前后端分离,不适用目前项目. 说明 前 ...

  6. Redis系列(五):数据结构List双向链表中基本操作操作命令和源码解析

    1.介绍 List是通过ListNode实现的双向链表. 1.双端:获取某个结点的前驱和后继结点都是O(1) 2.无环:表头的prev指针和表尾的next指针都指向NULL,对链表的访问都是以NULL ...

  7. Magicodes.IE在.NET Core中通过请求头导出多种格式文件

    前言 在2.2里程碑中我们增加了一些新的功能,正如标题所写通过请求头进行导出我们不同格式的文件.下面我们来看一下如何使用.通过这种方式无论是对我们的数据多用途,还是说对我们的数据校验都做到了轻松易配. ...

  8. Java 中的数据结构类 Stack

    JDK 中的 Stack 类便是经典的数据结构栈的实现,它继承于线程安全的 Vector 类,而且它自身的线程不安全的方法上也加上了 synchronized 关键字,所以它的内部操作也是线程安全的哦 ...

  9. git提交时报错:Updates were rejected because the tip of your current branch is behind

    有如下3种解决方法: 1.使用强制push的方法:git push -u origin master -f这样会使远程修改丢失,一般是不可取的,尤其是多人协作开发的时候. 2.push前先将远程rep ...

  10. 1.尚硅谷_MyBatis_简介.avi

    hibernate旨在消除mysql语句.程序员不写sql语言,要实现复杂的功能需要学习hibernate的hql语句 mybatis把编写sql语言交给程序员,程序员自己在xml控制sql语句的编写 ...