Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案

PEP 573 – 從 C 擴充套件方法訪問模組狀態

作者:
Petr Viktorin <encukou at gmail.com>, Alyssa Coghlan <ncoghlan at gmail.com>, Eric Snow <ericsnowcurrently at gmail.com>, Marcel Plch <gmarcel.plch at gmail.com>
BDFL 委託
Stefan Behnel
討論至:
Import-SIG 郵件列表
狀態:
最終版
型別:
標準跟蹤
建立日期:
2016年6月2日
Python 版本:
3.9
釋出歷史:


目錄

摘要

本 PEP 提議為 CPython 擴充套件方法新增一種訪問上下文的方式,例如它們所定義模組的狀態。

這將允許擴充套件方法使用直接指標解引用而不是 PyState_FindModule 來查詢模組狀態,從而減少或消除使用模組作用域狀態而非程序全域性狀態的效能開銷。

這解決了採用 PEP 3121(擴充套件模組初始化和終結)和 PEP 489(多階段擴充套件模組初始化)的剩餘障礙之一。

儘管本 PEP 朝著完全解決 PEP 3121PEP 489 開始解決的問題又邁出了一步,但它並不試圖解決所有剩餘問題。特別是,從槽方法(nb_add 等)訪問模組狀態的問題尚未解決。

術語

程序全域性狀態

C 級靜態變數。由於這是非常低階的記憶體儲存,因此必須仔細管理。

模組級狀態

模組物件本地的狀態,作為模組物件初始化的一部分動態分配。這使狀態與模組的其他例項(包括其他子直譯器中的例項)隔離。

透過 PyModule_GetState() 訪問。

靜態型別

定義為 C 級靜態變數的型別物件,即編譯入的型別物件。

靜態型別需要在模組例項之間共享,並且不包含其所屬模組的資訊。靜態型別沒有 __dict__(儘管其例項可能有)。

堆型別

在執行時建立的型別物件。

定義類

方法的定義類(無論是繫結還是非繫結)是定義該方法的類。僅僅從其基類繼承方法的類不是定義類。

例如,intTrue.to_bytesTrue.__floor__int.__repr__ 的定義類。

在 C 中,定義類是使用相應的 tp_methods 或“tp 槽”[1] 條目定義的類。對於在 Python 中定義的方法,定義類儲存在 __class__ 閉包單元中。

C-API

Python 文件中描述的“Python/C API”。CPython 實現了 C-API,但存在其他實現。

基本原理

PEP 489 引入了一種新的擴充套件模組初始化方式,它為實現它的擴充套件帶來了幾個優勢

  • 擴充套件模組的行為更像它們的 Python 對應物。
  • 擴充套件模組可以輕鬆支援載入到現有模組物件中,這為 runpy 或支援擴充套件模組重新載入的系統鋪平了道路。
  • 可以從同一個擴充套件載入多個模組,這使得可以在單個直譯器中測試模組隔離(正確子直譯器支援的關鍵特性)。

採用 PEP 489 的最大障礙是允許從擴充套件型別的方法訪問模組狀態。目前,從擴充套件方法訪問此狀態的方法是透過 PyState_FindModule 查詢模組(與擴充套件模組中的模組級函式相反,後者接收模組引用作為引數)。然而,PyState_FindModule 查詢執行緒本地狀態,與 C 級程序全域性訪問相比,開銷相對較高,因此阻礙了模組作者使用它。

此外,PyState_FindModule 依賴於在每個子直譯器中,最多隻有一個模組對應於給定的 PyModuleDef 的假設。對於使用 PEP 489 多階段初始化的模組,此假設不成立,因此 PyState_FindModule 不適用於這些模組。

需要一種更快、更安全的方式從擴充套件方法訪問模組級狀態。

背景

Python 方法的實現可能需要訪問以下一個或多個資訊

  • 它被呼叫的例項 (self)
  • 底層函式
  • 定義類,即定義該方法的類
  • 相應的模組
  • 模組狀態

在 Python 程式碼中,Python 級等價物可以這樣檢索

import sys

class Foo:
    def meth(self):
        instance = self
        module_globals = globals()
        module_object = sys.modules[__name__]  # (1)
        underlying_function = Foo.meth         # (1)
        defining_class = Foo                   # (1)
        defining_class = __class__             # (2)

注意

定義類不是 type(self),因為 type(self) 可能是 Foo 的子類。

標有 (1) 的語句隱式依賴於透過函式的 __globals__ 進行基於名稱的查詢:要麼是訪問定義類和 Python 函式物件的 Foo 屬性,要麼是查詢 sys.modules 中模組物件的 __name__

在 Python 程式碼中,這是可行的,因為在執行函式定義時,__globals__ 會適當設定,即使名稱空間已被操作以返回不同的物件,最壞的情況也會引發異常。

__class__ 閉包 (2) 是一種更安全的獲取定義類的方式,但它仍然依賴於 __closure__ 的適當設定。

相比之下,擴充套件方法通常作為普通 C 函式實現。這意味著它們只能訪問其引數以及 C 級執行緒區域性和程序全域性狀態。傳統上,許多擴充套件模組將其共享狀態儲存在 C 級程序全域性變數中,這在以下情況下會引發問題

  • 在同一程序中執行多個初始化/終結週期
  • 重新載入模組(例如,測試條件匯入)
  • 在子直譯器中載入擴充套件模組

PEP 3121 試圖透過提供 PyState_FindModule API 來解決此問題,但對於擴充套件方法(而非模組級函式)而言,這仍然存在重大問題

  • 它明顯慢於直接訪問 C 級程序全域性狀態
  • 它仍然固有地依賴於程序全域性狀態,這意味著它仍然不能可靠地處理模組重新載入

還有一種情況是,在查詢 C 級結構(如模組狀態)時,提供意外的物件佈局可能會使直譯器崩潰,因此確保擴充套件方法接收到它們期望的物件型別更為重要。

提案

目前,繫結擴充套件方法(PyCFunctionPyCFunctionWithKeywords)只接收 self,以及(如果適用)提供的位置和關鍵字引數。

雖然模組級擴充套件函式已經透過它們的 self 引數獲得了對定義模組物件的訪問許可權,但擴充套件型別的方法卻沒有這種便利:它們透過 self 接收繫結例項,因此無法直接訪問定義類或模組級狀態。

上述附加模組級上下文可以透過兩個更改來實現。這兩個新增功能都是可選的;擴充套件作者需要選擇啟用才能開始使用它們

  • 向堆型別物件新增指向模組的指標。
  • 將定義類傳遞給底層 C 函式。

    在 CPython 中,定義類在建立內建方法物件 (PyCFunctionObject) 時很容易獲得,因此可以將其儲存在一個擴充套件 PyCFunctionObject 的新結構中。

然後可以透過 PyModule_GetState 從模組物件檢索模組狀態。

請注意,此提議意味著任何需要訪問模組級狀態的型別都必須是堆型別,而不是靜態型別。這對於支援從單個擴充套件載入多個模組物件是必要的:靜態型別作為 C 級全域性變數,不包含其所屬模組物件的任何資訊。

槽方法

上述更改不涵蓋槽方法,例如 tp_iternb_add

槽方法的問題在於它們的 C API 是固定的,因此我們不能簡單地新增一個新引數來傳入定義類。對此問題提出了兩種可能的解決方案

  • 透過遍歷 MRO 查詢類。這可能開銷很大,但如果效能不是問題(例如在引發模組級異常時)則可以使用。
  • 將每個槽的定義類指標儲存在單獨的表 __typeslots__ [2] 中。這在技術上是可行的且快速,但侵入性很大。

受此問題影響的模組還可以選擇使用執行緒區域性狀態PEP 567 上下文變數作為快取機制,或者定義自己的重新載入友好查詢快取方案。

普遍解決此問題將推遲到未來的 PEP。

規範

向堆型別新增模組引用

C-API 將新增一個用於建立模組的工廠方法

PyObject* PyType_FromModuleAndSpec(PyObject *module,
                                   PyType_Spec *spec,
                                   PyObject *bases)

這與 PyType_FromSpecWithBases 的行為相同,此外還將提供的模組物件與新型別關聯起來。(在 CPython 中,這將設定下文描述的 ht_module。)

此外,還將提供一個訪問器 PyObject * PyType_GetModule(PyTypeObject *)。如果型別設定了關聯模組,它將返回該模組;否則,它將設定 TypeError 並返回 NULL。當給定靜態型別時,它將始終設定 TypeError 並返回 NULL。

為了在 CPython 中實現這一點,PyHeapTypeObject 結構將獲得一個新成員 PyObject *ht_module,它將儲存指向關聯模組的指標。它預設為 NULL,並且在建立型別物件後不應修改。

ht_module 成員不會被子類繼承;對於每個需要它的型別,都需要使用 PyType_FromModuleAndSpec 來設定它。

通常,建立設定了 ht_module 的類會建立涉及類和模組的引用迴圈。這不是問題,因為模組的拆解不是效能敏感的操作,並且模組級函式通常也會建立引用迴圈。透過 f_globals 打破函式迴圈的現有“將所有模組全域性變數設定為 None”程式碼也將打破透過 ht_module 形成的新迴圈。

將定義類傳遞給擴充套件方法

將新增一個新的簽名標誌 METH_METHOD,用於 PyMethodDef.ml_flags。從概念上講,它將 defining_class 新增到函式簽名中。為了簡化初始實現,該標誌只能用作 (METH_FASTCALL | METH_KEYWORDS | METH_METHOD)。(它不能與其他標誌(如 METH_O 或裸 METH_FASTCALL)一起使用,但可以與 METH_CLASSMETH_STATIC 組合使用)。

使用此標誌組合定義的方法的 C 函式將使用名為 PyCMethod 的新 C 簽名進行呼叫

PyObject *PyCMethod(PyObject *self,
                    PyTypeObject *defining_class,
                    PyObject *const *args,
                    size_t nargsf,
                    PyObject *kwnames)

未來(甚至在此 PEP 的初始實現中)可能會新增其他組合,如 (METH_VARARGS | METH_METHOD)。然而,METH_METHOD 應該始終是附加標誌,即只有在需要時才應傳入定義類。

在 CPython 中,將新增一個擴充套件 PyCFunctionObject 的新結構來儲存額外資訊

typedef struct {
    PyCFunctionObject func;
    PyTypeObject *mm_class; /* Passed as 'defining_class' arg to the C func */
} PyCMethodObject;

PyCFunction 實現發現設定了 METH_METHOD 標誌時,它會將 mm_class 傳遞給 PyCMethod C 函式。將新增一個新宏 PyCFunction_GET_CLASS(cls) 以更方便地訪問 mm_class

C 方法如果不需要訪問其定義類/模組,則可以繼續使用其他 METH_* 簽名。如果未設定 METH_METHOD,則轉換為 PyCMethodObject 是無效的。

引數診所

為了支援將定義類傳遞給使用 Argument Clinic 的方法,CPython 的 Argument Clinic 工具中將新增一個名為 defining_class 的新轉換器。

每個方法只能有一個引數使用此轉換器,並且它必須出現在 self 之後,或者如果未使用 self,則作為第一個引數。該引數的型別將為 PyTypeObject *

使用時,Argument Clinic 將選擇 METH_FASTCALL | METH_KEYWORDS | METH_METHOD 作為呼叫約定。該引數不會出現在 __text_signature__ 中。

新的轉換器最初將與 __init____new__ 方法不相容,後者不能使用 METH_METHOD 約定。

助手函式

從堆型別獲取模組級狀態是一項非常常見的任務。為了簡化此操作,將新增一個助手函式

void *PyType_GetModuleState(PyObject *type)

此函式接受一個堆型別,成功時,它返回指向該堆型別所屬模組狀態的指標。

失敗時,可能會出現兩種情況。當傳入非型別物件或沒有模組的型別時,TypeError 被設定並返回 NULL。如果找到模組,則返回指向狀態的指標(可能為 NULL),而不設定任何異常。

初始實現中轉換的模組

為了驗證該方法,在初始實現期間將修改 _elementtree 模組。

API 變更和新增功能摘要

以下內容將新增到 Python C-API 中

  • PyType_FromModuleAndSpec 函式
  • PyType_GetModule 函式
  • PyType_GetModuleState 函式
  • METH_METHOD 呼叫標誌
  • PyCMethod 函式簽名

以下新增內容將作為 CPython 實現細節新增,不會被記錄

  • PyCFunction_GET_CLASS
  • PyCMethodObject 結構
  • _heaptypeobjectht_module 成員
  • Argument Clinic 中的 defining_class 轉換器

向後相容性

所有堆型別都添加了一個新指標。所有其他更改都是新增新函式和結構,或更改私有實現細節。

實施

初始實現可在 Github 倉庫中找到 [3];補丁集位於 [4]

未來可能的擴充套件

槽方法

未來可能會增加一種將定義類(或模組狀態)傳遞給槽方法的方式。

此 PEP 的先前版本提出了一個輔助函式,該函式將透過在 MRO 中搜索定義了特定函式槽的類來確定定義類。但是,如果類被修改(對於堆型別,這在 Python 程式碼中是可能的),此方法將失敗。解決此問題留待未來的討論。

輕鬆建立帶有模組引用的型別

可以新增一個 PEP 489 執行槽型別,以使建立堆型別比呼叫 PyType_FromModuleAndSpec 容易得多。這留待未來的 PEP。

最好能有一種好方法透過受限 API 建立靜態異常型別。這樣的異常型別可以在子直譯器之間共享,但無需特定模組狀態即可例項化。這也留待未來可能的討論。

最佳化

如本文所述,使用 METH_METHOD 標誌定義的方法僅支援一種特定簽名。

如果出於效能原因需要其他簽名,可以新增。

參考資料


來源:https://github.com/python/peps/blob/main/peps/pep-0573.rst

最後修改:2025-02-01 08:59:27 GMT