Python 中的命名空间和作用域是什么?

2024年8月29日 | 阅读 10 分钟

在一个满是学生的教室里,至少有两名学生同名的可能性很高。我们如何称呼这些学生?我们会使用姓氏或别名来唯一识别每个人。在 Python 的对象教室中,需要个体身份。面对大量对象,Python 使用命名空间的概念来管理名称。在本教程中,我们将学习“命名空间”的含义以及更多概念,并附带示例。

Python 中的对象可以是任何东西,从变量到方法。Python 将所有对象的名称以字典的形式存储,其中名称作为键,对象作为值。

Dict = {名称1: 对象1, 名称2: 对象2…}

命名空间的定义

Python 中的命名空间是定义的对象及其名称的集合,以字典的形式定义。命名空间中的每个名称:对象(键:值)对都解释了一个对应的对象。

Python 有许多内置库,因此也有许多内置对象。要检查 Python 中的所有内置对象

  1. 打开 cmd
  2. 输入 python3 或 py
  3. 使用命令

输出

['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
  • builtins 是 Python 中的一个模块,其中包含 Python 中的所有内置标识符。我们使用 dir() 方法获取模块的属性和方法列表。
  • 这个命名空间称为内置命名空间,解释器在我们启动 Python 时创建它,并一直保留到我们终止会话。

Python 使用命名空间来识别对象。它如何识别两个同名的对象?

首先,Python 中有 3 种类型的命名空间

  1. 我们已经学习了内置命名空间
  2. 全局命名空间
  3. 局部命名空间

顾名思义

  1. 内置命名空间是 Python 中内置对象的名称集合
  2. 全局命名空间是程序主体中定义的自定义对象的名称集合。Python 还会为我们导入程序中的模块和库创建全局命名空间。
  3. 当 Python 发现函数调用并到达函数定义时,它会为函数创建一个命名空间。局部命名空间收集在函数体内部定义的自定义对象的名称。这些命名空间只在函数执行期间维护。

请看以下示例

  • 有一个函数 enclosing_function,在其主体中定义了另一个函数 enclosed_function。
  • 首先,enclosing_function 的名称存储在全局命名空间中,主程序调用 enclosing_function 并为其创建一个命名空间。
  • enclosed_function 的名称存储在 enclosing_function 的命名空间中。
  • enclosing_function 调用 enclosed_function 并为其创建一个命名空间。
  • 因此,上述示例中有两个局部命名空间。每个命名空间只存在直到相应的函数终止。
全局命名空间外层命名空间内层命名空间
Enclosing_functionEnclosed_function

请注意,在单个命名空间中,不能存在两个同名的同类型对象。在不同的命名空间中可以存在同名同类型的对象。因此,一个对象可以同时存在于多个命名空间中。

这里又有一个问题

当同名对象存在于多个命名空间中时,Python 如何找出我们指的是哪个对象?

答案在于一个叫做“作用域”的概念。

对象的范围是程序中它具有意义的部分。根据对象的定义位置和引用位置,解释器确定对象的范围。

当我们在程序中引用一个对象来查找它时,Python 遵循一个名为 LEGB 规则

L: 局部命名空间

E: 外层命名空间

G: 全局命名空间

B: 内置命名空间

假设我们引用了一个变量 a

  • 解释器首先在引用的局部函数体内部搜索。
  • 如果未找到 a,并且存在另一个函数包含局部函数,则它会在外层函数的体内搜索 a。
  • 然后,它会在全局命名空间中查找。
  • 最后,如果以上搜索均未成功,则它会在 Python 的内置命名空间中搜索。
  • 如果解释器在上述任何搜索中都找不到 a,则表示 a 未定义。它会引发 NameError 异常。

以下是一些示例

示例 1

输出

object

理解

在上面的程序中,我们在 enclosed_function() 内部引用 a。

命名空间

全局命名空间外层命名空间内层命名空间
Enclosing_function, aEnclosed_function

搜索将如下进行

  1. L: 内层命名空间:未找到
  2. E: 外层命名空间:未找到
  3. G: 全局命名空间:找到

示例 2

输出

Local object

理解

我们在 enclosed_function() 内部引用了 a

命名空间

全局命名空间外层命名空间内层命名空间
Enclosing_function, aEnclosed_functiona

搜索过程如下

1. L: 内层命名空间:找到

解释器打印内层命名空间中 a 的值,不再继续搜索。

示例 3

输出

enclosing

理解

我们在 enclosed_function() 内部引用了 a

命名空间

全局命名空间外层命名空间内层命名空间
Enclosing_function, aEnclosed_function, a

搜索过程如下

L: 内层命名空间:未找到

E: 外层命名空间:找到

示例 4

输出

NameError: name 'a' is not defined

理解

我们在 enclosing_function() 内部引用了 a

命名空间

全局命名空间外层命名空间内层命名空间
Enclosing_functionEnclosed_functiona

搜索过程如下

L: 外层命名空间:未找到

E: 无外层函数

G: 未找到

B: 未找到

因此,NameError

  • 即使 a 在内层命名空间中定义,正如我们前面讨论的,函数的命名空间只在函数终止之前维护。因此,内层函数的作用域被限制在其主体内。

既然我们了解了命名空间的类型,这里有一点需要注意

  • 全局和局部命名空间以字典的形式实现,正如我们前面讨论的。
  • 内置命名空间不是以字典的形式实现。它是以模块的形式实现的。

访问命名空间

我们之前已经在教程中使用了 dir(__builtins__) 访问了内置命名空间。现在我们将学习如何访问全局和局部命名空间

全局命名空间

我们可以使用名为 globals() 的内置函数访问全局命名空间

在交互模式 (cmd) 中

输出

{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': , '__spec__': None, '__annotations__': {}, '__builtins__': , 'a': 'Global'}

理解

'a' : '全局'

这里,a 是名称,'全局' 是对象。

我们也可以在 IDE 中使用该函数 -> print(globals())

这里是另一个例子

输出

{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': , '__spec__': None, '__annotations__': {}, '__builtins__': , 'a': 'Global', 'f': }

理解

我们创建了一个函数 f(),并在其中定义了一个局部变量 b。当我们调用 globals() 时

  1. a 在命名空间中,而 b 不在,因为 b 是一个局部变量。
  2. 函数的名称在命名空间中,请注意它只是名称,函数将有其局部命名空间,其中包含 b。

根据您的应用程序,字典可以有不同的名称。由于它是一个字典,我们可以执行一些操作,例如

  1. 比较 (is)
  2. 添加条目
  3. 修改现有条目

局部命名空间

与全局命名空间的 globals() 类似,有一个 locals() 函数用于访问局部命名空间。它应该在函数内部调用以获取函数的命名空间。如果它在函数外部使用,则它将表现得像 globals()。

这里有一个例子

输出

{'b': 1, 'c': 2}
3

一些重要的点需要掌握

  1. globals() 返回原始的全局命名空间字典。我们执行的每个更改和变量都会显示在原始全局命名空间中。您可以在上面的示例中观察到这一点。
  2. 另一方面,locals() 返回原始局部命名空间的副本。因此,即使我们进行任何更改,例如添加条目,我们也只会对我们使用的副本进行这些更改,而不会对原始局部命名空间进行更改。

这里有一个例子

输出

{'a': 3, 'b': 4}
{'a': 5, 'b': 4}
{'a': 3, 'b': 4, 'local': {...}}

理解

在上面的示例中,我们创建了一个函数 add()。Python 为函数 add() 创建了一个局部命名空间。我们将 locals() 字典存储在局部变量中。我们修改了局部变量。请注意,更改并未反映在原始局部命名空间中,我们局部创建的副本已添加到局部命名空间中。因此,我们无法像修改全局命名空间那样修改原始局部命名空间。

修改

我们了解到,两个同名对象可以存在于不同的作用域中。现在,我们将看到哪些参数可以在哪些作用域中修改,哪些不能。

这里有两个重要的点

  1. 不可变对象不能在其作用域之外修改。
  2. 可变对象可以修改,但不能重新赋值。

让我们通过一个例子来理解第一点

字符串是不可变的。我们在全局作用域中定义了一个字符串 a,并在函数 f() 内部尝试修改 a 的值。因此,打印的是全局作用域中 a 的值。

可变对象

列表是可变的。因此,它在局部作用域中被修改。

如果我们尝试重新分配一个可变对象

当我们在局部作用域中重新赋值 a 时,将创建一个新的局部对象,因为两个同名对象可以存储在两个不同的命名空间中。因此,当我们在全局命名空间中引用 a 时,打印的是全局命名空间中的 a。

Python 从不让我们束手无策。如果我们需要在另一个作用域中修改不可变对象:

  1. 使用 global 声明
  2. 使用 globals() 函数
  3. 使用 nonlocal 声明

1. global 声明

假设我们想在函数(局部作用域)内部修改一个全局对象。为了阻止 Python 创建新的局部对象,我们可以使用 global 引用来引用全局对象

通过语句 global a,我们告诉解释器,函数内部对 a 的任何引用实际上都是对全局对象 a 的引用。

如果引用一个甚至不在全局命名空间中的全局对象会怎样?

是的,Python 会创建该对象并将其放置在全局命名空间中。

2. globals() 函数

使用此函数,我们可以访问全局命名空间并更新对象

3. nonlocal 声明

如果我们要修改内层函数中外部函数的对象,可以使用 nonlocal 声明。全局声明或函数在这里不起作用,因为我们要修改的对象不在全局命名空间中,而是在外部函数的命名空间中。

这里有一个例子

  • 然而,不建议修改另一个作用域中的对象。我们可以使用带有返回值的函数。

结束清单

  1. 什么是命名空间?
  2. 命名空间的类型。
  3. 访问命名空间。
  4. Python 查找对象的方式。
  5. 修改对象。
  6. 修改其他作用域中的对象。