浅谈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装饰器在Django框架下去除冗余代码的教程
Apr 16 Python
pandas对指定列进行填充的方法
Apr 11 Python
Python cookbook(数据结构与算法)将多个映射合并为单个映射的方法
Apr 19 Python
使用python opencv对目录下图片进行去重的方法
Jan 12 Python
如何利用Python分析出微信朋友男女统计图
Jan 25 Python
Python面向对象之类和实例用法分析
Jun 08 Python
Python PIL图片添加字体的例子
Aug 22 Python
手把手教你pycharm专业版安装破解教程(linux版)
Sep 26 Python
python实现两个字典合并,两个list合并
Dec 02 Python
python保留小数位的三种实现方法
Jan 07 Python
matlab中二维插值函数interp2的使用详解
Apr 22 Python
python+opencv实现目标跟踪过程
Jun 21 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
长波知识介绍
2021/03/01 无线电
PHP 编程的 5个良好习惯
2009/02/20 PHP
PHP正则表达式入门教程(推荐)
2016/05/18 PHP
javascript 点击整页变灰的效果(可做退出效果)。
2008/01/09 Javascript
js从10种颜色中随机取色实现每次取出不同的颜色
2013/10/23 Javascript
JavaScript搜索字符串并将搜索结果返回到字符串的方法
2015/04/06 Javascript
js窗口关闭提示信息(兼容IE和firefox)
2015/10/23 Javascript
Vue.js 2.0 和 React、Augular等其他前端框架大比拼
2016/10/08 Javascript
javaScript嗅探执行神器-sniffer.js
2017/02/14 Javascript
详解.vue文件中监听input输入事件(oninput)
2017/09/19 Javascript
微信小程序如何获取手机验证码
2018/11/04 Javascript
element vue Array数组和Map对象的添加与删除操作
2018/11/14 Javascript
javascript中的数据类型检测方法详解
2019/08/07 Javascript
js 判断当前时间是否处于某个一个时间段内
2019/09/19 Javascript
js单线程的本质 Event Loop解析
2019/10/29 Javascript
js实现验证码干扰(静态)
2021/02/22 Javascript
符合语言习惯的 Python 优雅编程技巧【推荐】
2018/09/25 Python
python 划分数据集为训练集和测试集的方法
2018/12/11 Python
python 提取tuple类型值中json格式的key值方法
2018/12/31 Python
详解pandas安装若干异常及解决方案总结
2019/01/10 Python
python里运用私有属性和方法总结
2019/07/08 Python
python中for循环变量作用域及用法详解
2019/11/05 Python
python实现矩阵和array数组之间的转换
2019/11/29 Python
python脚本后台执行方式
2019/12/21 Python
Python3 A*寻路算法实现方式
2019/12/24 Python
Django使用Profile扩展User模块方式
2020/05/14 Python
高校毕业生自我鉴定
2013/10/27 职场文书
学校消防演习方案
2014/02/19 职场文书
中队活动总结
2014/08/27 职场文书
2014迎接教师节演讲稿
2014/09/10 职场文书
大足石刻导游词
2015/02/02 职场文书
小班上学期个人总结
2015/02/12 职场文书
研究生个人学年总结
2015/02/14 职场文书
2016七一建党节慰问信
2015/11/30 职场文书
介绍信应该怎么开?
2019/04/03 职场文书
三种方式清除vue路由跳转router-link的历史记录
2022/04/10 Vue.js