浅谈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三个数的和。ui

对上述函数进行验证:命令行

# 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版)》中的相关论述:code

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

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

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

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

若是在内置命名空间中也找不到匹配值,就会引起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
相关文章
相关标签/搜索