浅谈Python中的作用域规则和闭包


Posted in Python onMarch 20, 2018

在对Python中的闭包进行简单分析之前,我们先了解一下Python中的作用域规则。关于Python中作用域的详细知识,有很多的博文都进行了介绍。这里我们先从一个简单的例子入手。

Python中的作用域

假设在交互式命令行中定义如下的函数:

>>> a = 1
>>> def foo():
    b = 2
    c = 3
    print "locals: %s" % locals()
    return "result: %d" % (a + b +c)
>>> a = 1
>>> def foo():
    b = 2
    c = 3
    print "locals: %s" % locals()
    return "result: %d" % (a + b +c)

上述代码先给a赋值1,紧接着定义了一个函数:foo()。在函数foo()中我们定义了两个整数b和c,函数的返回值为a、b、c三个数的和。

对上述函数进行验证:

# result
>>> foo()
locals: {'c': 3, 'b': 2}
result: 6
# result
>>> foo()
locals: {'c': 3, 'b': 2}
result: 6

根据验证的结果,foo()函数的返回值为6。上述的函数定义中只有b和c两个变量的赋值,那调用函数是如何判断a的值呢?这涉及到函数的作用域规则。本文摘录《Python参考手册(第4版)》中的相关论述:

每次执行一个函数时, 就会创建心得局部命名空间。该命名空间代表一个局部环境,其中包含函数参数的名称和在函数体内赋值的变量名称。解析这些名称时:

解释器将首先搜索局部命名空间;

如果没有找到匹配的名称,它就会搜索全局命名空间(函数的全局命名空间始终是定义该函数的模块);

如果解释器在全局命名空间中也找不到匹配值,最终会检查内置命名空间;

如果在内置命名空间中也找不到匹配值,就会引发NameError异常。

对应于上面的例子,foo函数首先会在局部命名空间中找三个变量的匹配值。上述代码中的locals()方法给出了foo函数局部命名空间的内容。可以看出,局部命名空间是一个字典,包含b和c的值,这是因为我们在foo函数中定义了这两个变量。然而,局部命名空间中不包含a的值,所以就需要在全局命名空间中寻找。可以使用__globals__获取一个函数的局部命名空间。

# foo函数的全局命名空间
>>> foo.__globals__
{'a': 1, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000004613518>, '__doc__': None}
# foo函数的全局命名空间
>>> foo.__globals__
{'a': 1, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000004613518>, '__doc__': None}

foo函数的全局命名空间中包含了内置函数模块、foo函数、变量a以及其他的一些参数。由于在foo函数的全局命名空间中找到了变量a,foo函数便返回三个变量的和。

Python闭包

上述的Python作用域规则具有普遍性。然而,在Python中“一切皆对象”,函数也不例外。这也就是说可以把函数当作参数传递给其他的函数,也可以放在数据结构中,还可以作为函数的返回结果。在这种情况下,Python的作用域规则会发生什么变化呢?我们还是举一个例子:

>>> def foo():
    a = 1
    def bar():
      b = 2
      c = 3
      return a + b + c
    return bar

>>> def foo():
    a = 1
    def bar():
      b = 2
      c = 3
      return a + b + c
    return bar

在这个例子中,我们定义了一个函数foo,并对变量a赋值。不过与之前的例子不同的是,在函数foo中我们还嵌套了一个函数bar,并且还定义了两个变量,这个函数是作为函数foo的返回值。根据上面的作用域规则,函数foo的局部作用域既不是函数bar的局部作用域,也不是它的全局作用域,那函数bar能否正确匹配变量a的值呢?我们我们来验证一下这个函数是否能够正常运行。

# 调用函数foo()
>>> bar = foo()
# 返回值bar是一个函数
>>> bar
<function bar at 0x00000000045F3588>
# 调用bar()
>>> bar()
# 结果显示为三个变量之和
6

以上的验证结果说明,在上述嵌套的函数中,内部函数可以正确地引用外部函数的变量,即使外部的函数已经返回。

这种内部函数的局部作用域中可以访问外部函数局部作用域中变量的行为,我们称为: 闭包。内部函数可以访问外部函数变量的特点很像将外部函数的变量直接“打包”到内部函数中一样,我们也可以这样理解闭包:将组成函数的语句以及执行这些语句的环境“打包”在一起时得到的对象称为闭包。

和闭包相关的几个对象
为了了解闭包是怎么实现内部函数对外部函数变量的引用,还需要对闭包相关的几个对象进行介绍。关于这几个对象会涉及到Python的底层实现,本文中对此不加以详述,可以参考以下文章:

不过,为了直观地说明闭包的实现过程(不分析底层实现),这里先简单介绍以下code对象。code对象是指代码对象,表示编译成字节的的可执行Python代码,或者字节码。它有几个比较重要的属性:

co_name:函数的名称
co_nlocals: 函数使用的局部变量的个数
co_varnames: 一个包含局部变量名字的元组
co_cellvars: 是一个元组,包含嵌套的函数所引用的局部变量的名字
co_freevars: 是一个元组,保存使用了的外层作用域中的变量名
co_consts: 是一个包含字节码使用的字面量的元组

其中比较关键的是co_varnames和co_freevars两个属性。我们对上面的例子稍加修改:

Python

>>> def foo():
    a = 1
    b = 2
    def bar():
      return a + 1
    def bar2():
      return b + 2
    return bar
>>> bar = foo()
# 外层函数
>>> foo.func_code.co_cellvars
('a', 'b')
>>> foo.func_code.co_freevars
()
# 内层嵌套函数
>>> bar.func_code.co_cellvars
()
>>> bar.func_code.co_freevars
('a',)

>>> def foo():
    a = 1
    b = 2
    def bar():
      return a + 1
    def bar2():
      return b + 2
    return bar
>>> bar = foo()
# 外层函数
>>> foo.func_code.co_cellvars
('a', 'b')
>>> foo.func_code.co_freevars
()
# 内层嵌套函数
>>> bar.func_code.co_cellvars
()
>>> bar.func_code.co_freevars
('a',)

以上说明外层函数的code对象的co_cellvars保存了内部嵌套函数需要引用的变量的名字,而内层嵌套函数的code对象的co_freevars保存了需要引用外部函数作用域中的变量名字。具体来说,就是foo函数中嵌套了两个函数,它们都需要引用foo函数局部作用域中的变量,所以foo.func_code.co_cellvars便包含变量a和变量b的名称。而函数bar是foo的返回值,只引用了变量a,因此bar.func_code.co_freevars中便只包含变量a。

内部函数和外部函数的co_freevars、co_cellvars的对应关系,使得在函数编译过程中内部函数具有了一个闭包的特殊属性__closure__(底层中对此有相关实现)。__closure__属性是一个由cell对象组成的元组,包含了由多个作用域引用的变量。可以做以下验证:

>>> foo.__closure__   #None
# 内部函数bar对变量a的引用
>>> bar.__closure__
(<cell at 0x00000000044F6798: int object at 0x0000000003FA4B38>,)
# 内部函数bar引用的变量a的值
>>> bar.__closure__[0].cell_contents
1

本文简单讲解了PYTHON的闭包,作用域的基本知识,如果想详细了解,请在本站中查询Python中的作用域规则和闭包详解

Python 相关文章推荐
Python实现的生成自我描述脚本分享(很有意思的程序)
Jul 18 Python
Python定义函数功能与用法实例详解
Apr 08 Python
python3.4 将16进制转成字符串的实例
Jun 12 Python
Python学习笔记之迭代器和生成器用法实例详解
Aug 08 Python
python实现WebSocket服务端过程解析
Oct 18 Python
Python猜数字算法题详解
Mar 01 Python
python 串行执行和并行执行实例
Apr 30 Python
小 200 行 Python 代码制作一个换脸程序
May 12 Python
关于matplotlib-legend 位置属性 loc 使用说明
May 16 Python
运行python提示no module named sklearn的解决方法
Nov 29 Python
单身狗福利?Python爬取某婚恋网征婚数据
Jun 03 Python
Python连接Postgres/Mysql/Mongo数据库基本操作大全
Jun 29 Python
python如何实现反向迭代
Mar 20 #Python
python利用高阶函数实现剪枝函数
Mar 20 #Python
python flask中静态文件的管理方法
Mar 20 #Python
python web基础之加载静态文件实例
Mar 20 #Python
python如何修改装饰器中参数
Mar 20 #Python
python MySQLdb使用教程详解
Mar 20 #Python
django中的HTML控件及参数传递方法
Mar 20 #Python
You might like
VML绘图板②脚本--VMLgraph.js、XMLtool.js
2006/10/09 PHP
Yii2压缩PHP中模板代码的输出问题
2018/08/28 PHP
PHP5.0 TIDY_PARSE_FILE缓冲区溢出漏洞的解决方案
2018/10/14 PHP
php使用fputcsv实现大数据的导出操作详解
2020/02/27 PHP
jQuery 学习第六课 实现一个Ajax的TreeView
2010/05/17 Javascript
jQuery 遍历-nextUntil()方法以及prevUntil()方法的使用介绍
2013/04/26 Javascript
用javascript删除当前行,添加行(示例代码)
2013/11/25 Javascript
在JavaScript中操作时间之getUTCDate()方法的使用
2015/06/10 Javascript
jQuery里filter()函数与find()函数用法分析
2015/06/24 Javascript
Javascript实现鼠标框选操作  不是点击选取
2016/04/14 Javascript
AngularJS基础 ng-dblclick 指令用法
2016/08/01 Javascript
微信小程序 wxapp内容组件 text详细介绍
2016/10/31 Javascript
canvas快速绘制圆形、三角形、矩形、多边形方法介绍
2016/12/29 Javascript
jQuery实现优雅的弹窗效果(6)
2017/02/08 Javascript
微信小程序多张图片上传功能
2017/06/07 Javascript
vue-router路由参数刷新消失的问题解决方法
2017/06/17 Javascript
JavaScript判断日期时间差的实例代码
2018/03/01 Javascript
JS实现的对象去重功能示例
2019/06/04 Javascript
微信小程序点击生成朋友圈分享图(遇到的坑)
2020/06/17 Javascript
[02:46]完美世界DOTA2联赛PWL DAY4集锦
2020/11/03 DOTA
下载给定网页上图片的方法
2014/02/18 Python
Python发送Email方法实例
2014/08/21 Python
更改Ubuntu默认python版本的两种方法python-&gt; Anaconda
2016/12/18 Python
Python调用C++程序的方法详解
2017/01/24 Python
Python列表和元组的定义与使用操作示例
2017/07/26 Python
Python数据可视化:饼状图的实例讲解
2019/12/07 Python
python代码中怎么换行
2020/06/17 Python
HTML5拖拽的简单实例
2016/05/30 HTML / CSS
html5录音功能实战示例
2019/03/25 HTML / CSS
澳大利亚领先的宠物用品商店:VetSupply
2017/09/08 全球购物
体育学院毕业生自荐信
2013/11/03 职场文书
体育教师个人总结
2015/02/09 职场文书
唐山大地震的观后感
2015/06/05 职场文书
总经理致辞
2015/07/29 职场文书
2015年小学语文教师工作总结
2015/10/23 职场文书
Arthas排查Kubernetes中应用频繁挂掉重启异常
2022/02/28 MySQL