Python作用域与闭包机制解析

落日邮差
落日邮差
发布时间:2026-10-13 | 3人浏览过
原创

本文解析Python中的作用域查找顺序(局部、全局、内置)及闭包的形成机制。通过代码示例说明内部函数如何引用外部函数变量,并介绍code对象中co_cellvars与co_freevars属性的作用,揭示闭包底层实现逻辑。

在深入探讨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 相关产品和应用

全部下载

专题列表