4. 执行模型

4.1. 程序结构

Python 程序由代码块构成。一个 代码块 是一个作为单元执行的 Python 程序文本片段。以下是代码块:模块、函数体和类定义。交互式输入的每个命令都是一个代码块。脚本文件(作为标准输入传递给解释器或作为命令行参数指定给解释器的文件)是一个代码块。脚本命令(在解释器命令行上使用 -c 选项指定的命令)是一个代码块。从命令行使用 -m 参数作为顶级脚本(作为模块 __main__)运行的模块也是一个代码块。传递给内置函数 eval()exec() 的字符串参数是一个代码块。

代码块在 执行帧 中执行。帧包含一些管理信息(用于调试),并决定代码块执行完成后继续执行的位置和方式。

4.2. 命名和绑定

4.2.1. 命名绑定

名称 指向对象。名称由命名绑定操作引入。

以下结构绑定名称

  • 函数的形式参数,

  • 类定义,

  • 函数定义,

  • 赋值表达式,

  • 如果出现在赋值中,则为标识符的 目标

    • for 循环头,

    • with 语句中的 as 之后,except 子句,except* 子句,或在结构化模式匹配中的 as-模式中,

    • 在结构化模式匹配中的捕获模式中

  • import 语句。

  • type 语句。

  • 类型参数列表.

形式为 from ... import *import 语句会绑定导入模块中定义的所有名称,除了以下划线开头的名称。这种形式只能在模块级别使用。

出现在 del 语句中的目标也被认为为此目的而绑定(尽管实际语义是解除绑定名称)。

每个赋值或导入语句都发生在一个由类或函数定义或在模块级别(顶层代码块)定义的块中。

如果一个名称在一个块中被绑定,它就是该块的局部变量,除非声明为 nonlocalglobal。如果一个名称在模块级别被绑定,它就是一个全局变量。(模块代码块的变量是局部变量和全局变量。)如果一个变量在一个代码块中被使用但没有在那里定义,它就是一个 自由变量

程序文本中每个名称的出现都引用了由以下名称解析规则建立的该名称的 绑定

4.2.2. 名称解析

一个 作用域定义了一个名称在一个块中的可见性。如果一个局部变量在一个块中被定义,它的作用域包括该块。如果定义发生在一个函数块中,作用域扩展到定义块中包含的任何块,除非包含的块为该名称引入了不同的绑定。

当一个名称在一个代码块中被使用时,它使用最近的封闭作用域进行解析。所有这些对代码块可见的作用域的集合被称为该块的 环境

当一个名称根本找不到时,会引发 NameError 异常。如果当前作用域是一个函数作用域,并且该名称引用了一个在使用该名称的位置尚未绑定到值的局部变量,则会引发 UnboundLocalError 异常。 UnboundLocalErrorNameError 的子类。

如果在一个代码块中的任何地方发生名称绑定操作,该块中所有对该名称的使用都被视为对当前块的引用。当在一个块中使用一个名称之前它被绑定时,这会导致错误。这条规则很微妙。Python 缺乏声明,并允许名称绑定操作发生在一个代码块中的任何地方。代码块的局部变量可以通过扫描整个块的文本以查找名称绑定操作来确定。有关示例,请参阅 有关 UnboundLocalError 的常见问题解答条目

如果 global 语句出现在一个块中,该语句中指定的名称的所有使用都引用顶层命名空间中这些名称的绑定。名称在顶层命名空间中通过搜索全局命名空间(即包含代码块的模块的命名空间)和内置命名空间(模块 builtins 的命名空间)来解析。首先搜索全局命名空间。如果在那里找不到名称,则接下来搜索内置命名空间。如果在内置命名空间中也找不到名称,则在全局命名空间中创建新变量。global 语句必须在所有对列出名称的使用之前出现。

global 语句的作用域与同一块中的名称绑定操作相同。如果自由变量的最近封闭作用域包含一个 global 语句,则该自由变量被视为全局变量。

nonlocal 语句使相应的名称引用最近封闭函数作用域中先前绑定的变量。 如果给定名称在任何封闭函数作用域中不存在,则在编译时会引发 SyntaxError类型参数 不能使用 nonlocal 语句重新绑定。

模块的命名空间在第一次导入模块时自动创建。 脚本的主模块始终称为 __main__

类定义块和 exec()eval() 的参数在名称解析的上下文中是特殊的。 类定义是一个可执行语句,它可以使用和定义名称。 这些引用遵循名称解析的正常规则,但有一个例外,即未绑定的局部变量在全局命名空间中查找。 类定义的命名空间成为类的属性字典。 在类块中定义的名称的作用域仅限于类块;它不扩展到方法的代码块。 这包括推导式和生成器表达式,但不包括 注释作用域,它们可以访问其封闭类作用域。 这意味着以下将失败

class A:
    a = 42
    b = list(a + i for i in range(10))

但是,以下将成功

class A:
    type Alias = Nested
    class Nested: pass

print(A.Alias.__value__)  # <type 'A.Nested'>

4.2.3. 注释作用域

类型参数列表type 语句引入注释作用域,它们的行为与函数作用域基本相同,但有一些例外,将在下面讨论。 注释 目前不使用注释作用域,但预计在 Python 3.13 中使用注释作用域,届时 PEP 649 将被实现。

注释作用域在以下上下文中使用

  • 泛型类型别名 的类型参数列表。

  • 泛型函数 的类型参数列表。 泛型函数的注释在注释作用域内执行,但其默认值和装饰器不执行。

  • 泛型类 的类型参数列表。 泛型类的基类和关键字参数在注释作用域内执行,但其装饰器不执行。

  • 类型变量的边界和约束 (延迟评估)。

  • 类型别名的值 (延迟评估)。

注释作用域与函数作用域的不同之处在于

  • 注释作用域可以访问其封闭类命名空间。 如果注释作用域直接位于类作用域内,或者位于另一个直接位于类作用域内的注释作用域内,则注释作用域中的代码可以使用类作用域中定义的名称,就好像它直接在类主体中执行一样。 这与在类中定义的常规函数形成对比,常规函数无法访问类作用域中定义的名称。

  • 注释作用域中的表达式不能包含 yieldyield fromawait:= 表达式。 (这些表达式在注释作用域内包含的其他作用域中是允许的。)

  • 在注解作用域中定义的名称不能在内部作用域中使用 nonlocal 语句重新绑定。这仅包括类型参数,因为注解作用域中出现的其他语法元素不会引入新的名称。

  • 虽然注解作用域具有内部名称,但该名称不会反映在定义在该作用域内的对象的 __qualname__ 中。相反,此类对象的 __qualname__ 就像该对象是在封闭作用域中定义的一样。

在 3.12 版本中添加: 注解作用域是在 Python 3.12 中作为 PEP 695 的一部分引入的。

4.2.4. 延迟评估

通过 type 语句创建的类型别名的值是延迟评估的。这同样适用于通过 类型参数语法 创建的类型变量的边界和约束。这意味着它们在创建类型别名或类型变量时不会被评估。相反,它们只在需要解析属性访问时才会被评估。

示例

>>> type Alias = 1/0
>>> Alias.__value__
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> def func[T: 1/0](): pass
>>> T = func.__type_params__[0]
>>> T.__bound__
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

这里,只有在访问类型别名的 __value__ 属性或类型变量的 __bound__ 属性时才会引发异常。

这种行为主要用于引用在创建类型别名或类型变量时尚未定义的类型。例如,延迟评估允许创建相互递归的类型别名

from typing import Literal

type SimpleExpr = int | Parenthesized
type Parenthesized = tuple[Literal["("], Expr, Literal[")"]]
type Expr = SimpleExpr | tuple[SimpleExpr, Literal["+", "-"], Expr]

延迟评估的值在 注解作用域 中进行评估,这意味着出现在延迟评估值内部的名称的查找方式就像它们是在紧邻的封闭作用域中使用的一样。

在 3.12 版本中添加。

4.2.5. 内置函数和受限执行

CPython 实现细节: 用户不应该触碰 __builtins__;它严格来说是一个实现细节。想要覆盖内置命名空间中的值的使用者应该 import builtins 模块并适当地修改其属性。

与代码块执行相关的内置命名空间实际上是通过在其全局命名空间中查找名称 __builtins__ 来找到的;这应该是一个字典或一个模块(在后一种情况下,使用模块的字典)。默认情况下,当在 __main__ 模块中时,__builtins__ 是内置模块 builtins;当在任何其他模块中时,__builtins__builtins 模块本身的字典的别名。

4.2.6. 与动态特性的交互

自由变量的名称解析发生在运行时,而不是编译时。这意味着以下代码将打印 42

i = 10
def f():
    print(i)
i = 42
f()

函数 eval()exec() 无法访问完整的环境来解析名称。名称可以在调用者的局部和全局命名空间中解析。自由变量不会在最近的封闭命名空间中解析,而是在全局命名空间中解析。 [1] 函数 exec()eval() 有可选参数来覆盖全局和局部命名空间。如果只指定了一个命名空间,则它将用于两者。

4.3. 异常

异常是打破代码块正常控制流程的一种手段,用于处理错误或其他异常情况。异常在检测到错误的点被引发;它可以由周围的代码块或直接或间接调用发生错误的代码块的任何代码块处理

当 Python 解释器检测到运行时错误(例如除以零)时,它会引发异常。Python 程序也可以使用 raise 语句显式引发异常。异常处理程序使用 tryexcept 语句指定。该语句的 finally 子句可用于指定清理代码,该代码不处理异常,但在前一个代码中发生异常与否都会执行。

Python 使用错误处理的“终止”模型:异常处理程序可以找出发生了什么并继续在更外层的级别执行,但它无法修复错误的原因并重试失败的操作(除非从顶部重新进入有问题的代码段)。

当异常完全没有被处理时,解释器会终止程序的执行,或返回到其交互式主循环。在这两种情况下,它都会打印堆栈回溯,除非异常是 SystemExit

异常由类实例标识。 except 子句根据实例的类进行选择:它必须引用实例的类或其 非虚拟基类。实例可以被处理程序接收,并且可以携带有关异常情况的附加信息。

注意

异常消息不是 Python API 的一部分。它们的内容可能会在 Python 的不同版本之间发生变化,恕不另行通知,并且不应该被将在多个版本的解释器下运行的代码所依赖。

另请参阅第 The try statement 节中对 try 语句的描述,以及第 The raise statement 节中对 raise 语句的描述。

脚注