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

 更新时间:2018年03月20日 10:47:04   投稿:wdc  
本文简单讲解了PYTHON的闭包,作用域的基本知识配合实例简单明了,适合初学者

在对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实现信用卡系统(支持购物、转账、存取钱)

    这篇文章主要介绍了Python实现信用卡系统(支持购物、转账、存取钱)的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-06-06
  • Python基于HOG+SVM/RF/DT等模型实现目标人行检测功能

    Python基于HOG+SVM/RF/DT等模型实现目标人行检测功能

    这篇文章主要介绍了Python基于HOG+SVM/RF/DT等模型实现目标检测[行人检测],今天这里并不是说要做出怎样的效果,而是基于HOG+SVM来实践机器学习检测的流程,需要的朋友可以参考下
    2022-06-06
  • python对常见数据类型的遍历解析

    python对常见数据类型的遍历解析

    这篇文章主要介绍了python对常见数据类型的遍历解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08
  • python入门之语句(if语句、while语句、for语句)

    python入门之语句(if语句、while语句、for语句)

    这篇文章主要介绍了python入门之语句,主要包括if语句、while语句、for语句的使用,需要的朋友可以参考下
    2015-01-01
  • python Scrapy框架原理解析

    python Scrapy框架原理解析

    这篇文章主要介绍了python Scrapy框架原理的相关资料,帮助大家更好的理解和使用python爬虫,感兴趣的朋友可以了解下
    2021-01-01
  • Python人工智能实战之对话机器人的实现

    Python人工智能实战之对话机器人的实现

    本文将通过Python开发一个可以讲笑话的机器人,可以自由定制功能,想讲几个笑话就讲几个笑话。文中的示例代码讲解详细,感兴趣的可以动手试一试
    2022-02-02
  • Python实现简单石头剪刀布游戏

    Python实现简单石头剪刀布游戏

    这篇文章主要为大家详细介绍了Python实现简单的石头剪刀布的游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01
  • Pytorch搭建yolo3目标检测平台实现源码

    Pytorch搭建yolo3目标检测平台实现源码

    这篇文章主要为大家介绍了Pytorch搭建yolo3目标检测平台实现源码,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05
  • Python+seaborn实现联合分布图的绘制

    Python+seaborn实现联合分布图的绘制

    联合分布(Joint Distribution)图是一种查看两个或两个以上变量之间两两相互关系的可视化图,在数据分析操作中经常需要用到。本文将通过seaborn实现绘制联合分布图,需要的可以参考一下
    2023-02-02
  • python字符串,数值计算

    python字符串,数值计算

    这篇文章主要介绍了python字符串,数值计算的相关资料,需要的朋友可以参考下
    2016-10-10

最新评论