4. 执行模型

4.1. 程序结构

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

代码块在执行帧中执行。帧包含一些管理信息(用于调试)并确定代码块执行完成后执行如何继续。

4.2. 命名和绑定

4.2.1. 名称的绑定

名称 指的是对象。名称是通过名称绑定操作引入的。

以下构造会绑定名称

  • 函数的形参,

  • 类定义,

  • 函数定义,

  • 赋值表达式,

  • 在赋值中出现的标识符目标

    • for 循环头,

    • with 语句、except 子句、except* 子句或结构模式匹配中的 as 模式之后出现的 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__ 就像该对象是在封闭作用域中定义的一样。

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

3.13 版本更改: PEP 696 引入的那样,注解作用域也用于类型参数默认值。

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 版本之间发生变化,恕不另行通知,因此不应被将在多个解释器版本下运行的代码所依赖。

另请参阅 try 语句在 try 语句 部分的描述,以及 raise 语句在 raise 语句 部分的描述。

脚注