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 語句 部分的描述。

腳註