Appearance

Python中的作用域(Scope)和命名空间(Namespace)

geekbing2023-11-19Pythonpython

前言

闭包有很多高级应用,比如装饰器(decorators),但闭包这个概念有点难以理解,因为它涉及作用域和命名空间。

为了更好的理解闭包,先看看什么是作用域命名空间

作用域(Scope)

作用域是 Python 中某一个变量名字可以被识别的区域。在任何时刻,都有三个或四个嵌套的作用域层级:

  • 全局作用域(Global Scope):当前脚本的最外层。
  • 局部作用域(Local Scope):在函数或方法内,在函数内部定义了一个变量,它就属于这个作用域内。
  • 封闭作用域(Enclosing Scope):嵌套函数中,在当前函数外部,但又不属于全局的作用域。
  • 内置作用域(Built-in Scope): Python 内置对象的作用域(如len, str等)。

当你在函数内部查找变量时,Python 会按照以下顺序在作用域中进行搜索:

  1. 局部作用域
  2. 封闭作用域
  3. 全局作用域
  4. 内置作用域

命名空间(Namespace)

在 Python 中,命名空间是一个保存了变量名和它们对应对象的地方,每个变量名都映射到一个对象。

命名空间分为以下几个层级:

  • 局部命名空间(Local Namespace):函数内部的命名空间。
  • 全局变量空间(Global Namespace):模块的命名空间,包含导入的模块、函数定义等。
  • 内置命名空间(Built-in Namespace):Python 内置对象的命名空间,如printlen等。

当你在函数内部声明变量时,这些变量位于局部命名空间。函数外部定义的变量位于全局命名空间

作用域与命名空间的关系

每当你访问一个变量,Python 会遵循LEGB规则(Local, Enclosing, Global, Built-in)按顺序在不同的层级中,查找这个变量名对应的对象。这些不同层级就是作用域,而每个作用域对应着一个命名空间命名空间可以看作是一个存储变量名与对应对象映射关系的容器。

通过一个简单的例子来逐一理解这些概念:

# 这里定义了一个全局变量,它位于全局命名空间(Global Namespace)
global_var = "我是一个全局变量"

# len 是 Python 的内置函数,位于内置命名空间(Built-in Namespace)
# 我们会在后面覆盖它来展示内置作用域

def outer_function():
    # 这个变量位于 outer_function 的局部命名空间(Local Namespace)
    # 对于内部的 inner_function 来说,这个命名空间是它的封闭命名空间(Enclosing Namespace)
    enclosed_var = "我是一个封闭变量"

    def inner_function():
        # 这个变量位于 inner_function 的局部命名空间(Local Namespace)
        local_var = "我是一个局部变量"
        print(local_var)       # 访问局部变量
        print(enclosed_var)    # 访问封闭作用域中的变量
        print(global_var)      # 访问全局变量
        print(len([]))         # 访问内置作用域的函数

    # 调用内部函数
    inner_function()

# 覆盖内置的 len 函数,这将在全局命名空间中创建一个新的名字 len
len = "这是覆盖内置len函数的全局变量"

# 调用外部函数
outer_function()
  1. lobal_var 定义在最外层,属于全局作用域(Global Scope),位于全局命名空间(Global Namespace)。
  2. outer_function 定义了一个名为 enclosed_var 的变量,这个变量是函数outer_function的局部作用域(Local Scope),对于 inner_function 来说,这个作用域是封闭作用域(Enclosing Scope),变量位于封闭的局部命名空间中。
  3. local_varinner_function的局部变量,属于局部作用域(Local Scope),位于它的局部命名空间(Local Namespace)。
  4. len 是一个内置函数,它原本是内置作用域(Built-in Scope)的一部分,位于内置命名空间(Built-in Namespace)。但在这个例子中,我们在全局命名空间中又定义了一个同名的字符串变量 len,这会覆盖内置的 len 函数。

上面代码执行结果如下:

我是一个局部变量
我是一个封闭变量
我是一个全局变量
这是覆盖内置len函数的全局变量

你肯定会问,为什么上面描述命名空间时,没有提到封闭命名空间(Enclosing Namespace),这是因为在 Python 中,封闭命名空间这个术语通常不会单独使用,而是在讨论闭包和作用域的上下文中才会出现。

这个术语并不是官方文档中的标准术语,但可以被用来指代嵌套函数中外层函数的局部变量的集合,从而帮助更好的理解闭包是如何工作的。

封闭命名空间(Enclosing Namespace)的概念是相对于内部函数来说的。

在上面的例子中:

对于 outer_function来说,变量 enclosed_var 位于它的局部命名空间(Local Namespace),但是对于 inner_function来说,变量 enclosed_var 位于它的封闭命名空间(Enclosing Namespace)。

这种封闭命名空间的概念是闭包的基础,它允许内部函数访问外部函数的局部变量,即便外部函数的执行已经结束,这种结构使得内部函数可以保持外部函数状态的引用。

闭包与作用域的关系

闭包即一个函数内部定义了另一个函数,并且这个内部函数还引用了外部函数的变量。通常当外部函数执行完成后,其局部作用域的生命周期就结束了,局部变量也会被垃圾回收。

但是,如果内部函数继续存活,比如被作为返回值传递出去,那么这个内部函数仍然持有外部函数的变量引用。这些变量的生命周期会被延长,仍然可以在外部函数执行完毕后被内部函数访问,这就是闭包。

闭包的强大之处在于,即使外部函数的执行上下文已经不存在了,闭包依然保留了外部函数作用域中变量的状态。这就允许我们在 Python 中实现某些编程模式,比如工厂函数、装饰器等。

上次更新 3/31/2024, 6:29:32 AM