浅谈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在windows下创建隐藏窗口子进程的方法
Jun 04 Python
Python字符串特性及常用字符串方法的简单笔记
Jan 04 Python
python实现神经网络感知器算法
Dec 20 Python
python做量化投资系列之比特币初始配置
Jan 23 Python
对python中字典keys,values,items的使用详解
Feb 03 Python
python单线程文件传输的实例(C/S)
Feb 13 Python
python 视频逐帧保存为图片的完整实例
Dec 10 Python
如何在python开发工具PyCharm中搭建QtPy环境(教程详解)
Feb 04 Python
python3.7调试的实例方法
Jul 21 Python
学习Python需要哪些工具
Sep 04 Python
如何基于Python实现word文档重新排版
Sep 29 Python
安装pytorch时报sslerror错误的解决方案
May 17 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日期处理函数 整型日期格式
2011/01/12 PHP
CentOS安装php v8js教程
2015/02/26 PHP
windows7配置Nginx+php+mysql的详细教程
2016/09/04 PHP
PHP实现二维数组(或多维数组)转换成一维数组的常见方法总结
2019/12/04 PHP
javascript 折半查找字符在数组中的位置(有序列表)
2010/12/09 Javascript
JSON传递bool类型数据的处理方式介绍
2013/09/18 Javascript
怎么判断js脚本加载完成
2014/02/28 Javascript
使用jquery中height()方法获取各种高度大全
2014/04/02 Javascript
异步JS框架的作用以及实现方法
2015/10/29 Javascript
AngularJS中处理多个promise的方式
2016/02/02 Javascript
javascript 常用验证函数总结
2016/06/28 Javascript
自己封装的一个原生JS拖动方法(推荐)
2016/11/22 Javascript
微信小程序 免费SSL证书https、TLS版本问题的解决办法
2016/12/14 Javascript
Vue中的v-cloak使用解读
2017/03/27 Javascript
Windows下支持自动更新的Electron应用脚手架的方法
2018/12/24 Javascript
使用 Vue cli 3.0 构建自定义组件库的方法
2019/04/30 Javascript
微信小程序实现点击空白隐藏的方法示例
2019/08/13 Javascript
Python的shutil模块中文件的复制操作函数详解
2016/07/05 Python
利用Python循环(包括while&amp;for)各种打印九九乘法表的实例
2017/11/06 Python
利用Django内置的认证视图实现用户密码重置功能详解
2017/11/24 Python
浅谈Python实现贪心算法与活动安排问题
2017/12/19 Python
Python利用ORM控制MongoDB(MongoEngine)的步骤全纪录
2018/09/13 Python
利用Python如何实现一个小说网站雏形
2018/11/23 Python
Python使用itchat 功能分析微信好友性别和位置
2019/08/05 Python
python实现把两个二维array叠加成三维array示例
2019/11/29 Python
flask框架配置mysql数据库操作详解
2019/11/29 Python
浅谈keras的深度模型训练过程及结果记录方式
2020/01/24 Python
Python pip使用超时问题解决方案
2020/08/03 Python
css3实现的下拉菜单效果示例
2014/01/22 HTML / CSS
企业承诺书怎么写
2014/05/24 职场文书
飞机制造技术专业求职信
2014/07/27 职场文书
离职报告格式
2014/11/04 职场文书
2016元旦主持人经典开场白台词
2015/12/03 职场文书
校园文化艺术节开幕词
2016/03/04 职场文书
担保书怎么写 ?
2019/04/22 职场文书
python编写五子棋游戏
2021/05/25 Python