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

Python 增強提案 (Python Enhancement Proposals)

PEP 695 – 型別參數語法

作者:
Eric Traut <erictr at microsoft.com>
贊助人:
Guido van Rossum <guido at python.org>
討論於:
Typing-SIG 討論串
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
主題:
類型標註 (Typing)
建立日期:
2022-06-15
Python 版本:
3.12
公告歷史:
2022-06-20, 2022-12-04
決議:
Discourse 訊息

目錄

重要資訊

本 PEP 為一份歷史文件:請參閱 變異數推論型別別名型別參數列表type 陳述式 以及 註釋作用域 以獲取最新的規範與文件。權威的型別規範維護於 typing 規範網站;執行時期的型別行為描述於 CPython 文件中。

×

有關如何提議更改型別規格,請參閱 typing 規格更新流程

摘要

本 PEP 為泛型類別、函式或型別別名中的型別參數指定了一種改進的語法。它同時引入了一個用於宣告型別別名的新陳述式。

動機

PEP 484 將型別變數引入語言中。PEP 612 基於此概念引入了參數規格說明,而 PEP 646 則加入了可變參數型別變數。

儘管泛型與型別參數日益普及,但指定型別參數的語法對於 Python 來說仍顯得像是「外掛」上去的,這是 Python 開發者困惑的來源。

Python 靜態型別社群已達成共識,認為是時候提供一種與其他支援泛型的現代程式語言相似的正式語法了。

一項針對 25 個熱門 Python 型別化函式庫的分析顯示,14% 的模組使用了型別變數(特別是 typing.TypeVar 符號)。

困惑點

雖然型別變數的使用已變得廣泛,但其在程式碼中的指定方式卻是許多 Python 開發者困惑的來源。造成這種困惑的原因有幾個。

型別變數的作用域規則難以理解。型別變數通常分配在全域作用域內,但其語意意義僅在泛型類別、函式或型別別名的上下文中才有效。型別變數的單一執行時期實例可能會在多個泛型上下文中重複使用,且在每個上下文中具有不同的語意意義。本 PEP 建議透過在類別、函式或型別別名宣告陳述式內的自然位置宣告型別參數,來消除此困惑來源。

泛型型別別名常被誤用,因為開發者不清楚在使用型別別名時必須提供型別參數。這會導致隱含的 Any 型別參數,而這通常並非原意。本 PEP 建議增加新語法,使泛型型別別名宣告變得明確。

PEP 483PEP 484 為泛型類別內使用的型別變數引入了「變異數」(variance)的概念。型別變數可以是不變的(invariant)、共變的(covariant)或反變的(contravariant)。變異數是型別理論中的進階細節,大多數 Python 開發者對其並不了解,然而他們在定義第一個泛型類別時就必須面對這個概念。本 PEP 在很大程度上消除了大多數開發者在定義泛型類別時理解變異數概念的需求。

當泛型類別或型別別名使用多個型別參數時,型別參數的排序規則可能會令人困惑。它通常基於它們在類別或型別別名宣告陳述式中首次出現的順序。然而,這可以在類別定義中透過包含「Generic」或「Protocol」基底類別來覆寫。例如,在類別宣告 class ClassA(Mapping[K, V]) 中,型別參數排序為 K 然後是 V。但在類別宣告 class ClassB(Mapping[K, V], Generic[V, K]) 中,型別參數排序則為 V 然後是 K。本 PEP 建議在所有情況下使型別參數排序變得明確。

現今跨多個泛型上下文共享型別變數的做法導致了其他問題。現代編輯器提供了諸如「尋找所有引用」和「重新命名所有引用」等功能,這些功能在語意層面上運作。當一個型別參數在多個泛型類別、函式和型別別名之間共享時,所有引用在語意上都是等效的。

定義在全域作用域內的型別變數也需要給予一個以底線開頭的名稱,以表明該變數對模組是私有的。全域定義的型別變數也常被命名以指示其變異數,導致出現如「_T_contra」和「_KT_co」等繁瑣名稱。目前的型別變數分配機制還要求開發者在引號中提供冗餘名稱(例如 T = TypeVar("T"))。本 PEP 消除了對冗餘名稱和繁瑣變數名稱的需求。

現今定義型別參數需要從 typing 模組匯入 TypeVarGeneric 符號。在過去幾個 Python 版本中,已努力消除對常見用例匯入 typing 符號的需求,而本 PEP 進一步推動了這一目標。

摘要範例

在本 PEP 之前定義泛型類別看起來像這樣。

from typing import Generic, TypeVar

_T_co = TypeVar("_T_co", covariant=True, bound=str)

class ClassA(Generic[_T_co]):
    def method1(self) -> _T_co:
        ...

使用新語法,它看起來像這樣。

class ClassA[T: str]:
    def method1(self) -> T:
        ...

以下是現今泛型函式的範例。

from typing import TypeVar

_T = TypeVar("_T")

def func(a: _T, b: _T) -> _T:
    ...

以及新語法。

def func[T](a: T, b: T) -> T:
    ...

以下是現今泛型型別別名的範例。

from typing import TypeAlias

_T = TypeVar("_T")

ListOrSet: TypeAlias = list[_T] | set[_T]

以及使用新語法。

type ListOrSet[T] = list[T] | set[T]

規範

型別參數宣告

這是用於宣告泛型類別、函式和型別別名型別參數的新語法。該語法在類別、函式或型別別名名稱後的方括號內,新增了對逗號分隔型別參數列表的支援。

簡單(非可變參數)型別變數以純名稱宣告。可變參數型別變數前綴以 *(詳見 PEP 646)。參數規格說明前綴以 **(詳見 PEP 612)。

# This generic class is parameterized by a TypeVar T, a
# TypeVarTuple Ts, and a ParamSpec P.
class ChildClass[T, *Ts, **P]: ...

無需包含 Generic 作為基底類別。將其作為基底類別是透過型別參數的存在而隱含的,它將自動包含在類別的 __mro____orig_bases__ 屬性中。顯式使用 Generic 基底類別將導致執行時期錯誤。

class ClassA[T](Generic[T]): ...  # Runtime error

帶有型別參數的 Protocol 基底類別可能會產生執行時期錯誤。型別檢查器在這種情況下應產生錯誤,因為不需要使用型別參數,且該類別的型別參數順序不再由其在 Protocol 基底類別中的順序決定。

class ClassA[S, T](Protocol): ... # OK

class ClassB[S, T](Protocol[S, T]): ... # Recommended type checker error

泛型類別、函式或型別別名內的型別參數名稱在該類別、函式或型別別名內必須是唯一的。重複的名稱會在編譯時期產生語法錯誤。這與函式簽名內參數名稱必須唯一的規定一致。

class ClassA[T, *T]: ... # Syntax Error

def func1[T, **T](): ... # Syntax Error

類別型別參數名稱若以雙底線開頭會被修飾(mangled),以避免使類別內使用的名稱查找機制複雜化。然而,型別參數的 __name__ 屬性將保留未修飾的名稱。

上限規格說明

對於非可變參數型別參數,可以透過使用型別註釋運算式來指定「上限」(upper bound)型別。如果未指定上限,則假定上限為 object

class ClassA[T: str]: ...

指定的上限型別必須使用型別註釋中允許的運算式形式。更複雜的運算式形式應由型別檢查器標記為錯誤。允許使用帶引號的前向引用。

指定的上限型別必須是具體的。嘗試使用泛型型別應由型別檢查器標記為錯誤。這與型別檢查器對 TypeVar 建構子呼叫執行的現有規則一致。

class ClassA[T: dict[str, int]]: ...  # OK

class ClassB[T: "ForwardReference"]: ...  # OK

class ClassC[V]:
    class ClassD[T: dict[str, V]]: ...  # Type checker error: generic type

class ClassE[T: [str, int]]: ...  # Type checker error: illegal expression form

受限型別規格說明

PEP 484 引入了「受限型別變數」(constrained type variable)的概念,該變數被限制為一組兩個或多個型別。新語法透過使用包含兩個或多個型別的字面量元組運算式來支援這種類型的約束。

class ClassA[AnyStr: (str, bytes)]: ...  # OK

class ClassB[T: ("ForwardReference", bytes)]: ...  # OK

class ClassC[T: ()]: ...  # Type checker error: two or more types required

class ClassD[T: (str, )]: ...  # Type checker error: two or more types required

t1 = (bytes, str)
class ClassE[T: t1]: ...  # Type checker error: literal tuple expression required

如果指定的型別不是元組運算式,或者元組運算式包含型別註釋中不允許的複雜運算式形式,型別檢查器應產生錯誤。允許使用帶引號的前向引用。

class ClassF[T: (3, bytes)]: ...  # Type checker error: invalid expression form

指定的受限型別必須是具體的。嘗試使用泛型型別應由型別檢查器標記為錯誤。這與型別檢查器對 TypeVar 建構子呼叫執行的現有規則一致。

class ClassG[T: (list[S], str)]: ...  # Type checker error: generic type

界限與約束的執行時期表示

TypeVar 物件的上限和約束可透過 __bound____constraints__ 屬性在執行時期存取。對於透過新語法定義的 TypeVar 物件,這些屬性將變為惰性求值,詳見下文 惰性求值

泛型型別別名

我們建議引入一個宣告型別別名的新陳述式。與 classdef 陳述式類似,type 陳述式定義了型別參數的作用域。

# A non-generic type alias
type IntOrStr = int | str

# A generic type alias
type ListOrSet[T] = list[T] | set[T]

型別別名可以在不使用引號的情況下引用自身。

# A type alias that includes a forward reference
type AnimalOrVegetable = Animal | "Vegetable"

# A generic self-referential type alias
type RecursiveList[T] = T | list[RecursiveList[T]]

type 關鍵字是一個新的軟關鍵字。它僅在語法的此部分被解釋為關鍵字。在所有其他位置,它被假定為識別碼名稱。

作為泛型型別別名一部分宣告的型別參數,僅在評估型別別名右側時有效。

typing.TypeAlias 一樣,型別檢查器應將右側運算式限制為型別註釋中允許的運算式形式。使用更複雜的運算式形式(呼叫運算式、三元運算子、算術運算子、比較運算子等)應被標記為錯誤。

型別別名運算式不允許使用傳統型別變數(即那些透過顯式 TypeVar 建構子呼叫分配的變數)。型別檢查器在這種情況下應產生錯誤。

T = TypeVar("T")
type MyList = list[T]  # Type checker error: traditional type variable usage

我們建議棄用 PEP 613 中引入的現有 typing.TypeAlias。新語法完全消除了其需求。

執行時期型別別名類別

在執行時期,type 陳述式將產生一個 typing.TypeAliasType 的實例。該類別代表了該型別。其屬性包括

  • __name__ 為代表型別別名名稱的字串(str)
  • __type_params__ 為一組 TypeVarTypeVarTupleParamSpec 物件的元組,若型別別名是泛型的,則對其進行參數化
  • __value__ 為型別別名的評估後值

所有這些屬性皆為唯讀。

型別別名的值會被惰性求值(見下文 惰性求值)。

型別參數作用域

當使用新語法時,會引入一個新的詞彙作用域(lexical scope),此作用域包含型別參數。型別參數可以在內部作用域中按名稱存取。與 Python 中的其他符號一樣,內部作用域可以定義自己的符號來覆寫同名的外部作用域符號。本節提供了新作用域規則的文字描述。下方的 作用域行為 節以轉換為等效的現有 Python 程式碼形式指定了該行為。

型別參數對列表中其他地方宣告的其他型別參數是可見的。這允許型別參數在其定義中使用其他型別參數。雖然目前沒有用到此功能,但它保留了未來支援依賴先前型別參數的上限運算式或預設型別參數的能力。

如果先前型別參數的定義引用了稍後的型別參數,即使名稱定義在外部作用域中,也會產生編譯器錯誤或執行時期例外。

# The following generates no compiler error, but a type checker
# should generate an error because an upper bound type must be concrete,
# and ``Sequence[S]`` is generic. Future extensions to the type system may
# eliminate this limitation.
class ClassA[S, T: Sequence[S]]: ...

# The following generates no compiler error, because the bound for ``S``
# is lazily evaluated. However, type checkers should generate an error.
class ClassB[S: Sequence[T], T]: ...

作為泛型類別一部分宣告的型別參數,在類別主體及其中包含的內部作用域內有效。當評估構成類別定義的參數列表(基底類別和任何關鍵字參數)時,型別參數也是可存取的。這允許基底類別由這些型別參數進行參數化。型別參數在類別主體之外不可存取,包括類別裝飾器。

class ClassA[T](BaseClass[T], param = Foo[T]): ...  # OK

print(T)  # Runtime error: 'T' is not defined

@dec(Foo[T])  # Runtime error: 'T' is not defined
class ClassA[T]: ...

作為泛型函式一部分宣告的型別參數,在函式主體及其中包含的任何作用域內有效。它在參數和回傳值型別註釋內也有效。函式參數的預設引數值是在此作用域之外評估的,因此型別參數在預設值運算式中不可存取。同樣,型別參數在函式裝飾器的作用域內不可用。

def func1[T](a: T) -> T: ...  # OK

print(T)  # Runtime error: 'T' is not defined

def func2[T](a = list[T]): ...  # Runtime error: 'T' is not defined

@dec(list[T])  # Runtime error: 'T' is not defined
def func3[T](): ...

作為泛型型別別名一部分宣告的型別參數,在型別別名運算式內有效。

type Alias1[K, V] = Mapping[K, V] | Sequence[K]

外部作用域中定義的型別參數符號不能與內部作用域中的 nonlocal 陳述式綁定。

S = 0

def outer1[S]():
    S = 1
    T = 1

    def outer2[T]():

        def inner1():
            nonlocal S  # OK because it binds variable S from outer1
            nonlocal T  # Syntax error: nonlocal binding not allowed for type parameter

        def inner2():
            global S  # OK because it binds variable S from global scope

新型別參數語法引入的詞彙作用域不同於由 defclass 陳述式引入的傳統作用域。型別參數作用域更像是對包含作用域的臨時「覆蓋」。其符號表中唯一的內容是使用新語法定義的型別參數。對所有其他符號的引用被視為在包含作用域內找到。這允許基底類別列表(在類別定義中)和型別註釋運算式(在函式定義中)引用在包含作用域內定義的符號。

class Outer:
    class Private:
        pass

    # If the type parameter scope was like a traditional scope,
    # the base class 'Private' would not be accessible here.
    class Inner[T](Private, Sequence[T]):
        pass

    # Likewise, 'Inner' would not be available in these type annotations.
    def method1[T](self, a: Inner[T]) -> Inner[T]:
        return a

編譯器允許內部作用域定義覆寫外部作用域型別參數的區域符號。

PEP 484 中定義的作用域規則一致,如果內部作用域的泛型類別、函式或型別別名重複使用了與外部作用域相同的型別參數名稱,型別檢查器應產生錯誤。

T = 0

@decorator(T)  # Argument expression `T` evaluates to 0
class ClassA[T](Sequence[T]):
    T = 1

    # All methods below should result in a type checker error
    # "type parameter 'T' already in use" because they are using the
    # type parameter 'T', which is already in use by the outer scope
    # 'ClassA'.
    def method1[T](self):
        ...

    def method2[T](self, x = T):  # Parameter 'x' gets default value of 1
        ...

    def method3[T](self, x: T):  # Parameter 'x' has type T (scoped to method3)
        ...

在內部作用域中引用的符號使用現有規則解析,只是在名稱解析過程中也會考慮型別參數作用域。

T = 0

# T refers to the global variable
print(T)  # Prints 0

class Outer[T]:
    T = 1

    # T refers to the local variable scoped to class 'Outer'
    print(T)  # Prints 1

    class Inner1:
        T = 2

        # T refers to the local type variable within 'Inner1'
        print(T)  # Prints 2

        def inner_method(self):
            # T refers to the type parameter scoped to class 'Outer';
            # If 'Outer' did not use the new type parameter syntax,
            # this would instead refer to the global variable 'T'
            print(T)  # Prints 'T'

    def outer_method(self):
        T = 3

        # T refers to the local variable within 'outer_method'
        print(T)  # Prints 3

        def inner_func():
            # T refers to the variable captured from 'outer_method'
            print(T)  # Prints 3

當泛型類別使用新型別參數語法時,類別定義的參數列表中不允許使用賦值運算式。同樣,對於使用新型別參數語法的函式,參數或回傳值型別註釋內不允許使用賦值運算式,在定義型別別名的運算式內,或在 TypeVar 的界限和約束內也不允許。類似地,這些上下文中不允許使用 yieldyield fromawait 運算式。

此限制是必要的,因為在新詞彙作用域內評估的運算式不應引入除定義的型別參數之外的符號,也不應影響包含函式是否為產生器(generator)或協程(coroutine)。

class ClassA[T]((x := Sequence[T])): ...  # Syntax error: assignment expression not allowed

def func1[T](val: (x := int)): ...  # Syntax error: assignment expression not allowed

def func2[T]() -> (x := Sequence[T]): ...  # Syntax error: assignment expression not allowed

type Alias1[T] = (x := list[T])  # Syntax error: assignment expression not allowed

在執行時期存取型別參數

泛型類別、函式和型別別名上有一個名為 __type_params__ 的新屬性。此屬性是一個包含參數化類別、函式或別名的型別參數的元組。該元組包含 TypeVarParamSpecTypeVarTuple 實例。

使用新語法宣告的型別參數不會出現在 globals()locals() 傳回的字典中。

變異數推論

本 PEP 消除了為型別參數指定變異數的需求。相反地,型別檢查器將基於型別參數在類別中的使用方式來推論其變異數。根據使用方式,型別參數將被推論為不變的、共變的或反變的。

Python 型別檢查器已經具備為了驗證泛型協定類別(generic protocol class)中的變異數而確定型別參數變異數的能力。此功能可用於所有類別(無論是否為協定)以計算每個型別參數的變異數。

計算型別參數變異數的演算法如下。

對於泛型類別中的每個型別參數

1. 如果型別參數是可變參數(TypeVarTuple)或參數規格說明(ParamSpec),它總是視為不變的。無需進一步推論。

2. 如果型別參數來自傳統的 TypeVar 宣告,且未指定為 infer_variance(見下文),其變異數由 TypeVar 建構子呼叫指定。無需進一步推論。

3. 建立兩個類別的特殊化版本。我們將它們稱為 upperlower 特殊化。在這兩種特殊化中,將所有除被推論者之外的型別參數替換為虛設型別實例(一個與自身型別相容且假定滿足型別參數界限或約束的具體匿名類別)。在 upper 特殊化類別中,使用 object 實例特殊化目標型別參數。此特殊化忽略了型別參數的上限或約束。在 lower 特殊化類別中,使用其自身特殊化目標型別參數(即對應的型別參數就是該型別參數本身)。

4. 使用標準型別相容性規則確定 lower 是否可以賦值給 upper。如果是,目標型別參數是共變的。如果不是,則確定 upper 是否可以賦值給 lower。如果是,目標型別參數是反變的。如果這兩種組合都不可賦值,則目標型別參數是不變的。

以下是一個範例。

class ClassA[T1, T2, T3](list[T1]):
    def method1(self, a: T2) -> None:
        ...

    def method2(self) -> T3:
        ...

為了確定 T1 的變異數,我們將 ClassA 特殊化如下

upper = ClassA[object, Dummy, Dummy]
lower = ClassA[T1, Dummy, Dummy]

我們發現使用 PEP 484 中定義的標準型別相容性規則,upper 不可賦值給 lower。同樣地,lower 也不可賦值給 upper,因此我們得出結論 T1 是不變的。

為了確定 T2 的變異數,我們將 ClassA 特殊化如下

upper = ClassA[Dummy, object, Dummy]
lower = ClassA[Dummy, T2, Dummy]

由於 upper 可以賦值給 lower,因此 T2 是反變的。

為了確定 T3 的變異數,我們將 ClassA 特殊化如下

upper = ClassA[Dummy, Dummy, object]
lower = ClassA[Dummy, Dummy, T3]

由於 lower 可以賦值給 upper,因此 T3 是共變的。

TypeVar 的自動變異數

現有的 TypeVar 類別建構子接受名為 covariantcontravariant 的關鍵字參數。如果兩者皆為 False,則型別變數被假定為不變的。我們建議增加另一個名為 infer_variance 的關鍵字參數,表示型別檢查器應使用推論來確定型別變數是不變的、共變的還是反變的。相應的實例變數 __infer_variance__ 可在執行時期存取以確定變異數是否為推論所得。使用新語法隱式分配的型別變數將始終將 __infer_variance__ 設定為 True

使用傳統語法的泛型類別可能包含具有顯式和推論變異數的型別變數組合。

T1 = TypeVar("T1", infer_variance=True)  # Inferred variance
T2 = TypeVar("T2")  # Invariant
T3 = TypeVar("T3", covariant=True)  # Covariant

# A type checker should infer the variance for T1 but use the
# specified variance for T2 and T3.
class ClassA(Generic[T1, T2, T3]): ...

與傳統 TypeVar 的相容性

現有的 TypeVarTypeVarTupleParamSpec 分配機制保留以用於向後相容性。然而,這些「傳統」型別變數不應與使用新語法分配的型別參數混合使用。此類混合應由型別檢查器標記為錯誤。這是必要的,因為型別參數順序不明確。

如果類別、函式或型別別名不使用新語法,則將傳統型別變數與新型別參數混合是可以的。在此情況下,新型別參數必須來自外部作用域。

K = TypeVar("K")

class ClassA[V](dict[K, V]): ...  # Type checker error

class ClassB[K, V](dict[K, V]): ...  # OK

class ClassC[V]:
    # The use of K and V for "method1" is OK because it uses the
    # "traditional" generic function mechanism where type parameters
    # are implicit. In this case V comes from an outer scope (ClassC)
    # and K is introduced implicitly as a type parameter for "method1".
    def method1(self, a: V, b: K) -> V | K: ...

    # The use of M and K are not allowed for "method2". A type checker
    # should generate an error in this case because this method uses the
    # new syntax for type parameters, and all type parameters associated
    # with the method must be explicitly declared. In this case, ``K``
    # is not declared by "method2", nor is it supplied by a new-style
    # type parameter defined in an outer scope.
    def method2[M](self, a: M, b: K) -> M | K: ...

執行時期實作

語法變更

本 PEP 引入了一個新的軟關鍵字 type。它透過以下方式修改語法

  1. classdef 陳述式中增加可選的型別參數子句。
type_params: '[' t=type_param_seq  ']'

type_param_seq: a[asdl_typeparam_seq*]=','.type_param+ [',']

type_param:
    | a=NAME b=[type_param_bound]
    | '*' a=NAME
    | '**' a=NAME

type_param_bound: ":" e=expression

# Grammar definitions for class_def_raw and function_def_raw are modified
# to reference type_params as an optional syntax element. The definitions
# of class_def_raw and function_def_raw are simplified here for brevity.

class_def_raw: 'class' n=NAME t=[type_params] ...

function_def_raw: a=[ASYNC] 'def' n=NAME t=[type_params] ...
  1. 增加用於定義型別別名的新 type 陳述式。
type_alias: "type" n=NAME t=[type_params] '=' b=expression

AST 變更

本 PEP 引入了一個名為 TypeAlias 的新 AST 節點型別。

TypeAlias(expr name, typeparam* typeparams, expr value)

它還增加了一個代表型別參數的 AST 節點型別。

typeparam = TypeVar(identifier name, expr? bound)
    | ParamSpec(identifier name)
    | TypeVarTuple(identifier name)

界限和約束在 AST 中表示相同。在實作中,任何作為 Tuple AST 節點的運算式被視為約束,任何其他運算式被視為界限。

它還修改了現有的 AST 節點型別 FunctionDefAsyncFunctionDefClassDef,包含一個額外的可選屬性 typeparams,其中包括與該函式或類別相關聯的型別參數列表。

惰性求值

本 PEP 引入了三個可能出現代表靜態型別之運算式的新上下文:TypeVar 界限、TypeVar 約束以及型別別名的值。這些運算式可能包含尚未定義的名稱引用。例如,型別別名可能是遞迴的,甚至是相互遞迴的,而型別變數界限可能引用回當前類別。如果這些運算式被立即評估(eagerly evaluated),使用者將需要用引號括住這些運算式以防止執行時期錯誤。PEP 563PEP 649 詳細說明了這種情況下對於型別註釋的問題。

為了防止本 PEP 提案的新語法出現類似情況,我們建議對這些運算式使用惰性求值,類似於 PEP 649 中的方法。具體而言,每個運算式將儲存在一個程式碼物件(code object)中,且該程式碼物件僅在對應屬性被存取時才會被評估(TypeVar.__bound__TypeVar.__constraints__TypeAlias.__value__)。在值成功評估後,該值將被保存,後續呼叫將傳回相同的值,而無需重新評估程式碼物件。

如果 PEP 649 得到實作,則應增加額外的評估機制,以反映該 PEP 為註釋提供的選項。在當前 PEP 版本中,這可能包括為 TypeVar 增加一個 __evaluate_bound__ 方法,該方法帶有一個與 PEP 649 的 __annotate__ 方法含義相同的 format 參數(以及類似的 __evaluate_constraints__ 方法,以及 TypeAliasType 上的 __evaluate_value__ 方法)。然而,在 PEP 649 被接受並實作之前,僅支援預設評估格式(PEP 649 的「VALUE」格式)。

作為惰性求值的結果,屬性觀察到的值可能取決於屬性被存取的時間。

X = int

class Foo[T: X, U: X]:
    t, u = T, U

print(Foo.t.__bound__)  # prints "int"
X = str
print(Foo.u.__bound__)  # prints "str"

可以使用 PEP 563 或 PEP 649 的語意構建影響型別註釋的類似範例。

惰性求值的原始實作會錯誤地處理類別命名空間,因為類別內的函式通常無法存取封閉類別的命名空間。該實作將保留對類別命名空間的引用,以便正確解析類別作用域名稱。

作用域行為

新語法需要一種行為不同於 Python 現有作用域的新作用域。因此,新語法無法完全根據現有的 Python 作用域行為來描述。本節透過引用現有的作用域行為進一步指定這些作用域:新作用域的行為類似於函式作用域,除了下列少數微小差異。

所有範例皆包含使用虛擬關鍵字 def695 引入的函式。此關鍵字在實際語言中並不存在;它用於釐清新作用域在很大程度上就像函式作用域。

def695 作用域在以下方面與一般函式作用域不同

  • 如果一個 def695 作用域直接位於類別作用域內,或位於另一個直接位於類別作用域內的 def695 作用域內,則在該類別作用域中定義的名稱可以在 def695 作用域內存取。(相比之下,一般函式無法存取在封閉類別作用域內定義的名稱。)
  • 以下構造在 def695 作用域內直接禁止,儘管它們可以在嵌套在 def695 作用域內的其它作用域中使用
    • yield
    • yield from
    • await
    • :=(海象運算子)
  • def695 作用域內定義的物件(類別和函式)的限定名稱(__qualname__)就像這些物件是在最接近的封閉作用域中定義的一樣。
  • def695 作用域內綁定的名稱無法在嵌套作用域中透過 nonlocal 陳述式重新綁定。

def695 作用域用於評估本 PEP 中提議的幾個新語法構造。有些是立即評估的(當型別別名、函式或類別被定義時);另一些則是惰性求值的(僅在明確要求評估時)。在所有情況下,作用域語意皆相同

  • 立即評估的值
    • 泛型型別別名的型別參數
    • 泛型函式的型別參數和註釋
    • 泛型類別的型別參數和基底類別運算式
  • 惰性求值的值
    • 泛型型別別名的值
    • 型別變數的界限
    • 型別變數的約束

在下方的轉換中,以兩個底線開頭的名稱是實作內部的,且對實際 Python 程式碼不可見。我們使用下列內部函式,在實際實作中,這些函式直接定義在直譯器中

  • __make_typealias(*, name, type_params=(), evaluate_value):建立一個帶有給定名稱、型別參數和惰性求值值的新 typing.TypeAlias 物件。該值直到存取 __value__ 屬性時才會被評估。
  • __make_typevar_with_bound(*, name, evaluate_bound):建立一個帶有給定名稱和惰性求值界限的新 typing.TypeVar 物件。該界限直到存取 __bound__ 屬性時才會被評估。
  • __make_typevar_with_constraints(*, name, evaluate_constraints):建立一個帶有給定名稱和惰性求值約束的新 typing.TypeVar 物件。這些約束直到存取 __constraints__ 屬性時才會被評估。

非泛型型別別名轉換如下

type Alias = int

等效於

def695 __evaluate_Alias():
    return int

Alias = __make_typealias(name='Alias', evaluate_value=__evaluate_Alias)

泛型型別別名

type Alias[T: int] = list[T]

等效於

def695 __generic_parameters_of_Alias():
    def695 __evaluate_T_bound():
        return int
    T = __make_typevar_with_bound(name='T', evaluate_bound=__evaluate_T_bound)

    def695 __evaluate_Alias():
        return list[T]
    return __make_typealias(name='Alias', type_params=(T,), evaluate_value=__evaluate_Alias)

Alias = __generic_parameters_of_Alias()

泛型函式

def f[T](x: T) -> T:
    return x

等效於

def695 __generic_parameters_of_f():
    T = typing.TypeVar(name='T')

    def f(x: T) -> T:
        return x
    f.__type_params__ = (T,)
    return f

f = __generic_parameters_of_f()

一個更完整的泛型函式範例,說明預設值、裝飾器和界限的作用域行為。請注意,此範例沒有正確使用 ParamSpec,因此應被靜態型別檢查器拒絕。然而,它在執行時期是有效的,此處用於說明執行時期語意。

@decorator
def f[T: int, U: (int, str), *Ts, **P](
    x: T = SOME_CONSTANT,
    y: U,
    *args: *Ts,
    **kwargs: P.kwargs,
) -> T:
    return x

等效於

__default_of_x = SOME_CONSTANT  # evaluated outside the def695 scope
def695 __generic_parameters_of_f():
    def695 __evaluate_T_bound():
        return int
    T = __make_typevar_with_bound(name='T', evaluate_bound=__evaluate_T_bound)

    def695 __evaluate_U_constraints():
        return (int, str)
    U = __make_typevar_with_constraints(name='U', evaluate_constraints=__evaluate_U_constraints)

    Ts = typing.TypeVarTuple("Ts")
    P = typing.ParamSpec("P")

    def f(x: T = __default_of_x, y: U, *args: *Ts, **kwargs: P.kwargs) -> T:
        return x
    f.__type_params__ = (T, U, Ts, P)
    return f

f = decorator(__generic_parameters_of_f())

泛型類別

class C[T](Base):
    def __init__(self, x: T):
        self.x = x

等效於

def695 __generic_parameters_of_C():
    T = typing.TypeVar('T')
    class C(Base):
        __type_params__ = (T,)
        def __init__(self, x: T):
            self.x = x
   return C

C = __generic_parameters_of_C()

def695 作用域與現有行為最大的差異在於類別作用域內的行為。這種差異是必要的,以便在類別內定義的泛型以直觀的方式運作

class C:
    class Nested: ...
    def generic_method[T](self, x: T, y: Nested) -> T: ...

等效於

class C:
    class Nested: ...

    def695 __generic_parameters_of_generic_method():
        T = typing.TypeVar('T')

        def generic_method(self, x: T, y: Nested) -> T: ...
        return generic_method

    generic_method = __generic_parameters_of_generic_method()

在此範例中,xy 的註釋是在 def695 作用域內評估的,因為它們需要存取泛型方法的型別參數 T。然而,它們還需要存取在類別命名空間內定義的 Nested 名稱。如果 def695 作用域表現得像一般函式作用域,Nested 在函式作用域內將不可見。因此,直接位於類別作用域內的 def695 作用域可以存取該類別作用域,如上所述。

函式庫變更

typing 模組中目前以 Python 實作的幾個類別必須部分在 C 中實作。這包括 TypeVarTypeVarTupleParamSpecGeneric,以及新的類別 TypeAliasType(如上所述)。實作可能會將與模組其餘部分高度互動的某些行為委託給 Python 版的 typing.py。這些類別的記錄行為不應改變。

參考實作

此提案已在 CPython PR #103764 中進行原型設計。

Pyright 型別檢查器支援本 PEP 中描述的行為。

否決的想法

前綴子句

我們探討了在 defclass 陳述式之前指定型別參數的各種語法選項。我們考慮過的一個變體使用 using 子句,如下所示

using S, T
class ClassA: ...

此選項被拒絕,因為型別參數的作用域規則較不清晰。此外,該語法與 Python 中常見的類別和函式裝飾器配合得不好。只有另一種熱門程式語言 C++ 使用這種方法。

我們同樣考慮了看起來像裝飾器的前綴形式(例如 @using(S, T))。此想法被拒絕,因為此類形式會與一般裝飾器混淆,且它們與現有裝飾器組合得不好。此外,裝飾器在邏輯上是在它們裝飾的陳述式之後執行的,因此若它們引入在「被裝飾」陳述式內可見的符號(型別參數)會令人困惑,因為被裝飾陳述式在邏輯上是在裝飾器之前執行的。

角括號

許多支援泛型的語言使用角括號。(參閱附錄 A 末尾的表格進行總結。)我們探討了在 Python 中使用角括號進行型別參數宣告,但最終因兩個原因拒絕了它。首先,Python 掃描器不將角括號視為「配對」符號,因此 <> 標記之間的換行字元會被保留。這意味著型別參數列表中的任何換行符都需要使用難看且繁瑣的 \ 跳脫序列。其次,Python 已經建立了使用方括號進行泛型型別顯式特殊化的慣例(例如 list[int])。我們認為,將角括號用於泛型宣告,而用方括號用於顯式特殊化是不一致且令人困惑的。我們調查的所有其他語言在這方面都是一致的。

界限語法

我們探討了指定型別變數界限和約束的各種語法選項。我們考慮過但最終拒絕了使用像 Scala 那樣的 <: 標記、使用像其他各種語言那樣的 extendswith 關鍵字,以及使用類似於現今 typing.TypeVar 建構子的函式呼叫語法。簡單的冒號語法與許多其他程式語言一致(參閱附錄 A),並且受到了受訪 Python 開發者群體的高度青睞。

顯式變異數

我們考慮過增加語法以指定型別參數預期是不變的、共變的還是反變的。Python 中的 typing.TypeVar 機制要求這麼做。包括 Scala 和 C# 在內的其他幾種語言也要求開發者指定變異數。我們拒絕了這個想法,因為變異數通常是可以推論出來的,且大多數現代程式語言確實會根據使用情況來推論變異數。變異數是一個許多開發者認為困惑的進階主題,因此我們希望為大多數 Python 開發者消除理解這個概念的需求。

名稱修飾 (Name Mangling)

在考慮實作選項時,我們考慮過「名稱修飾」(name mangling)方法,其中每個型別參數由編譯器給予唯一的「修飾」名稱。此修飾名稱將基於與其相關聯的泛型類別、函式或型別別名的限定名稱。此方法被拒絕,因為限定名稱不一定是唯一的,這意味著修飾名稱需要基於某種其他隨機化值。此外,此方法與用於評估帶引號(前向引用)型別註釋的技術不相容。

附錄 A:型別參數語法調查

許多程式語言皆支援泛型。在本節中,我們對其他熱門程式語言使用的選項進行了調查。這是有意義的,因為熟悉其他語言將使 Python 開發者更容易理解此概念。我們在此處提供更多細節(例如預設型別參數支援),這些細節在考慮 Python 型別系統的未來擴充時可能很有用。

C++

C++ 使用角括號結合 templatetypename 關鍵字來宣告型別參數。它使用角括號進行特殊化。

C++20 引入了廣義約束的概念,它可以像 Python 中的協定一樣運作。一組約束可以定義在稱為 concept 的命名實體中。

變異數未明確指定,但約束可以強制執行變異數。

可以使用 = 運算子指定預設型別參數。

// Generic class
template <typename T>
class ClassA
{
    // Constraints are supported through compile-time assertions.
    static_assert(std::is_base_of<BaseClass, T>::value);

public:
    Container<T> t;
};

// Generic function with default type argument
template <typename S = int>
S func1(ClassA<S> a, S b) {};

// C++20 introduced a more generalized notion of "constraints"
// and "concepts", which are named constraints.

// A sample concept
template<typename T>
concept Hashable = requires(T a)
{
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// Use of a concept in a template
template<Hashable T>
void func2(T value) {}

// Alternative use of concept
template<typename T> requires Hashable<T>
void func3(T value) {}

// Alternative use of concept
template<typename T>
void func3(T value) requires Hashable<T> {}

Java

Java 使用角括號宣告型別參數和特殊化。預設情況下,型別參數是不變的。 extends 關鍵字用於指定上限。super 關鍵字用於指定反變界限。

Java 使用使用處變異數(use-site variance)。編譯器根據泛型型別的使用情況限制可以存取哪些方法和成員。變異數未明確指定。

Java 沒有提供指定預設型別參數的方法。

// Generic class
public class ClassA<T> {
    public Container<T> t;

    // Generic method
    public <S extends Number> void method1(S value) { }

    // Use site variance
    public void method1(ClassA<? super Integer> value) { }
}

C#

C# 使用角括號宣告型別參數和特殊化。 where 關鍵字和冒號用於指定型別參數的界限。

C# 使用宣告處變異數(declaration-site variance),分別使用 inout 關鍵字來分別表示反變和共變。預設情況下,型別參數是不變的。

C# 沒有提供指定預設型別參數的方法。

// Generic class with bounds on type parameters
public class ClassA<S, T>
    where T : SomeClass1
    where S : SomeClass2
{
    // Generic method
    public void MyMethod<U>(U value) where U : SomeClass3 { }
}

// Contravariant and covariant type parameters
public class ClassB<in S, out T>
{
    public T MyMethod(S value) { }
}

TypeScript

TypeScript 使用角括號宣告型別參數和特殊化。 extends 關鍵字用於指定界限。它可以與諸如 keyof 之類的其他型別運算子結合使用。

TypeScript 使用宣告處變異數。變異數是從使用情況中推論出來的,而非明確指定。TypeScript 4.7 引入了使用 inout 關鍵字指定變異數的能力。這是為了處理變異數推論成本極高的極複雜型別而增加的。

可以使用 = 運算子指定預設型別參數。

TypeScript 支援使用 type 關鍵字宣告型別別名,且此語法支援泛型。

// Generic interface
interface InterfaceA<S, T extends SomeInterface1> {
    val1: S;
    val2: T;

    method1<U extends SomeInterface2>(val: U): S
}

// Generic function
function func1<T, K extends keyof T>(ojb: T, key: K) { }

// Contravariant and covariant type parameters (TypeScript 4.7)
interface InterfaceB<in S, out T> { }

// Type parameter with default
interface InterfaceC<T = SomeInterface3> { }

// Generic type alias
type MyType<T extends SomeInterface4> = Array<T>

Scala

在 Scala 中,方括號用於宣告型別參數。方括號也用於特殊化。<:>: 運算子分別用於指定上限和下限。

Scala 使用使用處變異數,但也允許宣告處變異數規格說明。它分別使用 +- 前綴運算子表示共變和反變。

Scala 沒有提供指定預設型別參數的方法。

它確實支援高階型別(接受型別參數作為型別參數的型別)。

// Generic class; type parameter has upper bound
class ClassA[A <: SomeClass1]
{
    // Generic method; type parameter has lower bound
    def method1[B >: A](val: B) ...
}

// Use of an upper and lower bound with the same type parameter
class ClassB[A >: SomeClass1 <: SomeClass2] { }

// Contravariant and covariant type parameters
class ClassC[+A, -B] { }

// Higher-kinded type
trait Collection[T[_]]
{
    def method1[A](a: A): T[A]
    def method2[B](b: T[B]): B
}

// Generic type alias
type MyType[T <: Int] = Container[T]

Swift

Swift 使用角括號宣告型別參數和特殊化。型別參數的上限使用冒號指定。

Swift 不支援泛型變異數;所有型別參數皆為不變的。

Swift 沒有提供指定預設型別參數的方法。

// Generic class
class ClassA<T> {
    // Generic method
    func method1<X>(val: T) -> X { }
}

// Type parameter with upper bound constraint
class ClassB<T: SomeClass1> {}

// Generic type alias
typealias MyType<A> = Container<A>

Rust

Rust 使用角括號宣告型別參數和特殊化。型別參數的上限使用冒號指定。或者,where 子句可以指定各種約束。

Rust 沒有傳統的物件導向繼承或變異數。Rust 中的子型別(subtyping)非常受限,且僅因生命週期(lifetimes)相關的變異數而發生。

可以使用 = 運算子指定預設型別參數。

// Generic class
struct StructA<T> { // T's lifetime is inferred as covariant
    x: T
}

fn f<'a>(
    mut short_lifetime: StructA<&'a i32>,
    mut long_lifetime: StructA<&'static i32>,
) {
    long_lifetime = short_lifetime;
    // error: StructA<&'a i32> is not a subtype of StructA<&'static i32>
    short_lifetime = long_lifetime;
    // valid: StructA<&'static i32> is a subtype of StructA<&'a i32>
}

// Type parameter with bound
struct StructB<T: SomeTrait> {}

// Type parameter with additional constraints
struct StructC<T>
where
    T: Iterator,
    T::Item: Copy
{}

// Generic function
fn func1<T>(val: &[T]) -> T { }

// Generic type alias
type MyType<T> = StructC<T>;

Kotlin

Kotlin 使用角括號宣告型別參數和特殊化。預設情況下,型別參數是不變的。型別的上限使用冒號指定。或者,where 子句可以指定各種約束。

Kotlin 支援宣告處變異數,其中型別參數的變異數使用 inout 關鍵字明確宣告。它也支援使用處變異數,限制可使用哪些方法和成員。

Kotlin 沒有提供指定預設型別參數的方法。

// Generic class
class ClassA<T>

// Type parameter with upper bound
class ClassB<T : SomeClass1>

// Contravariant and covariant type parameters
class ClassC<in S, out T>

// Generic function
fun <T> func1(): T {

    // Use site variance
    val covariantA: ClassA<out Number>
    val contravariantA: ClassA<in Number>
}

// Generic type alias
typealias TypeAliasFoo<T> = ClassA<T>

Julia

Julia 使用大括號宣告型別參數和特殊化。<: 運算子可用於 where 子句中,以宣告型別的上限和下限。

# Generic struct; type parameter with upper and lower bounds
# Valid for T in (Int64, Signed, Integer, Real, Number)
struct Container{Int <: T <: Number}
    x::T
end

# Generic function
function func1(v::Container{T}) where T <: Real end

# Alternate forms of generic function
function func2(v::Container{T} where T <: Real) end
function func3(v::Container{<: Real}) end

# Tuple types are covariant
# Valid for func4((2//3, 3.5))
function func4(t::Tuple{Real,Real}) end

Dart

Dart 使用角括號宣告型別參數和特殊化。型別的上限使用 extends 關鍵字指定。預設情況下,型別參數是共變的。

Dart 支援宣告處變異數,其中型別參數的變異數使用 inoutinout 關鍵字明確宣告。它不支援使用處變異數。

Dart 沒有提供指定預設型別參數的方法。

// Generic class
class ClassA<T> { }

// Type parameter with upper bound
class ClassB<T extends SomeClass1> { }

// Contravariant and covariant type parameters
class ClassC<in S, out T> { }

// Generic function
T func1<T>() { }

// Generic type alias
typedef TypeDefFoo<T> = ClassA<T>;

Go

Go 使用方括號宣告型別參數和特殊化。型別的上限在參數名稱後指定,且必須始終指定。關鍵字 any 用於未受限的型別參數。

Go 不支援變異數;所有型別參數皆為不變的。

Go 沒有提供指定預設型別參數的方法。

Go 不支援泛型型別別名。

// Generic type without a bound
type TypeA[T any] struct {
    t T
}

// Type parameter with upper bound
type TypeB[T SomeType1] struct { }

// Generic function
func func1[T any]() { }

摘要

宣告語法 上限 下限 預設值 變異數位置 變異數
C++ template <> 不適用 不適用 = 不適用 不適用
Java <> extends use super, extends
C# <> where decl in, out
TypeScript <> extends = decl inferred, in, out
Scala [] T <: X T >: X use, decl +, -
Swift <> T: X 不適用 不適用
Rust <> T: X, where = 不適用 不適用
Kotlin <> T: X, where use, decl in, out
Julia {} T <: X X <: T 不適用 不適用
Dart <> extends decl in, out, inout
Go [] T X 不適用 不適用
Python (提案) [] T: X decl inferred (推論)

致謝

感謝 Sebastian Rittau 啟動了導致此提案的討論,感謝 Jukka Lehtosalo 提議型別別名陳述式的語法,並感謝 Jelle Zijlstra、Daniel Moisset 和 Guido van Rossum 對規範和實作提出的寶貴回饋與改進建議。


原始來源: https://github.com/python/peps/blob/main/peps/pep-0695.rst

最後修改: 2025-07-07 12:42:34 GMT