浅谈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 30 Python
解决在Python编辑器pycharm中程序run正常debug错误的问题
Jan 17 Python
Python 常用模块 re 使用方法详解
Jun 06 Python
python提取照片坐标信息的实例代码
Aug 14 Python
Win10+GPU版Pytorch1.1安装的安装步骤
Sep 27 Python
python基于FTP实现文件传输相关功能代码实例
Sep 28 Python
python隐藏类中属性的3种实现方法
Dec 19 Python
pandas的resample重采样的使用
Apr 24 Python
pyecharts在数据可视化中的应用详解
Jun 08 Python
python批量修改交换机密码的示例
Sep 22 Python
Python常用断言函数实例汇总
Nov 30 Python
Python第三方库安装缓慢的解决方法
Feb 06 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
php curl 登录163邮箱并抓取邮箱好友列表的代码(经测试)
2011/04/07 PHP
ThinkPHP与PHPExcel冲突解决方法
2011/08/08 PHP
Mysql的Root密码忘记,查看或修改的解决方法(图文介绍)
2013/06/14 PHP
PHP中new static()与new self()的区别异同分析
2014/08/22 PHP
修改WordPress中文章编辑器的样式的方法详解
2015/12/15 PHP
PHP让网站移动访问更加友好方法
2019/02/14 PHP
json原理分析及实例介绍
2012/11/29 Javascript
datagrid框架的删除添加与修改
2013/04/08 Javascript
Javascript学习笔记之 函数篇(一) : 函数声明和函数表达式
2014/06/24 Javascript
JavaScript中的类(Class)详细介绍
2014/12/30 Javascript
基于JavaScript实现类似于百度学术高级检索功能
2016/03/02 Javascript
基于JavaScript实现智能右键菜单
2016/03/02 Javascript
深入理解JavaScript中的对象复制(Object Clone)
2016/05/18 Javascript
AngularJS Ajax详解及示例代码
2016/08/17 Javascript
jQuery实现可拖拽3D万花筒旋转特效
2017/01/03 Javascript
如何使node也支持从url加载一个module详解
2018/06/05 Javascript
React如何解决fetch跨域请求时session失效问题
2018/11/02 Javascript
elementUI vue this.$confirm 和el-dialog 弹出框 移动 示例demo
2019/07/03 Javascript
[01:42]辉夜杯战队访谈宣传片—FANTUAN
2015/12/25 DOTA
详解Python3中yield生成器的用法
2015/08/20 Python
Python对CSV、Excel、txt、dat文件的处理
2018/09/18 Python
对Python3 序列解包详解
2019/02/16 Python
使用pyqt 实现重复打开多个相同界面
2019/12/13 Python
详解pandas绘制矩阵散点图(scatter_matrix)的方法
2020/04/23 Python
MAC平台基于Python Appium环境搭建过程图解
2020/08/13 Python
神话般的珠宝:Ross-Simons
2020/07/13 全球购物
运动会邀请函范文
2014/01/31 职场文书
有创意的广告词
2014/03/18 职场文书
家长通知书教师评语
2014/04/17 职场文书
文员试用期转正自我鉴定
2014/09/14 职场文书
党的群众路线教育实践活动个人整改落实情况汇报
2014/10/28 职场文书
2015年企业新年寄语
2014/12/08 职场文书
小学教师师德师风承诺书
2015/04/28 职场文书
XX部保密工作制度范本
2019/08/27 职场文书
详解redis分布式锁的这些坑
2021/05/19 Redis
MyBatis配置文件解析与MyBatis实例演示
2022/04/07 Java/Android