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

Python 增強提案 (Python Enhancement Proposals)

PEP 696 – 類型參數的預設值

作者:
James Hilton-Balfe <gobot1234yt at gmail.com>
贊助人:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
討論於:
Discourse 討論串
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
主題:
類型標註 (Typing)
建立日期:
2022年7月14日
Python 版本:
3.13
公告歷史:
2022年3月22日, 2023年1月8日
決議:
Discourse 訊息

目錄

重要資訊

本 PEP 是一份歷史性文件:請參閱 類型參數的預設值類型參數列表 以獲取最新的規範與說明文件。標準型別規範維護於 typing 規範網站;執行時期的型別行為描述於 CPython 說明文件中。

×

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

摘要

本 PEP 引入了類型參數的預設值概念,包含 TypeVarParamSpecTypeVarTuple,這些預設值在未指定具體類型時會自動生效。

預設型別參數支援已存在於 C++、TypeScript 與 Rust 等熱門語言中。針對常見語言的類型參數語法調查由 PEP 695 的作者進行,詳見該文件的 附錄 A

動機

T = TypeVar("T", default=int)  # This means that if no type is specified T = int

@dataclass
class Box(Generic[T]):
    value: T | None = None

reveal_type(Box())                      # type is Box[int]
reveal_type(Box(value="Hello World!"))  # type is Box[str]

這項需求 經常出現 的場景之一是 Generator。我提議將其 stub 定義 修改為如下形式:

YieldT = TypeVar("YieldT")
SendT = TypeVar("SendT", default=None)
ReturnT = TypeVar("ReturnT", default=None)

class Generator(Generic[YieldT, SendT, ReturnT]): ...

Generator[int] == Generator[int, None] == Generator[int, None, None]

這對於通常只針對單一類型使用的 Generic 也很有用。

class Bot: ...

BotT = TypeVar("BotT", bound=Bot, default=Bot)

class Context(Generic[BotT]):
    bot: BotT

class MyBot(Bot): ...

reveal_type(Context().bot)         # type is Bot  # notice this is not Any which is what it would be currently
reveal_type(Context[MyBot]().bot)  # type is MyBot

這不僅能改善明確使用者的型別檢查體驗,也能幫助依賴自動完成 (auto-complete) 功能來加速開發的非型別檢查使用者。

此設計模式在以下專案中很常見:

  • discord.py — 上述範例即取自此處。
  • NumPy — 例如 ndarraydtype 預設值應為 float64。目前其為 UnknownAny
  • TensorFlow — 這可用於 Tensor,類似於 numpy.ndarray,且有助於簡化 Layer 的定義。

規範

預設值的順序與訂閱規則

預設值的順序應遵循標準函數參數規則,因此沒有 default 的類型參數不能跟隨在具有 default 值的參數之後。這樣做在理想情況下應會在 typing._GenericAlias/types.GenericAlias 中引發 TypeError,且類型檢查器應將其標記為錯誤。

DefaultStrT = TypeVar("DefaultStrT", default=str)
DefaultIntT = TypeVar("DefaultIntT", default=int)
DefaultBoolT = TypeVar("DefaultBoolT", default=bool)
T = TypeVar("T")
T2 = TypeVar("T2")

class NonDefaultFollowsDefault(Generic[DefaultStrT, T]): ...  # Invalid: non-default TypeVars cannot follow ones with defaults


class NoNonDefaults(Generic[DefaultStrT, DefaultIntT]): ...

(
    NoNoneDefaults ==
    NoNoneDefaults[str] ==
    NoNoneDefaults[str, int]
)  # All valid


class OneDefault(Generic[T, DefaultBoolT]): ...

OneDefault[float] == OneDefault[float, bool]  # Valid
reveal_type(OneDefault)          # type is type[OneDefault[T, DefaultBoolT = bool]]
reveal_type(OneDefault[float]()) # type is OneDefault[float, bool]


class AllTheDefaults(Generic[T1, T2, DefaultStrT, DefaultIntT, DefaultBoolT]): ...

reveal_type(AllTheDefaults)                  # type is type[AllTheDefaults[T1, T2, DefaultStrT = str, DefaultIntT = int, DefaultBoolT = bool]]
reveal_type(AllTheDefaults[int, complex]())  # type is AllTheDefaults[int, complex, str, int, bool]
AllTheDefaults[int]  # Invalid: expected 2 arguments to AllTheDefaults
(
    AllTheDefaults[int, complex] ==
    AllTheDefaults[int, complex, str] ==
    AllTheDefaults[int, complex, str, int] ==
    AllTheDefaults[int, complex, str, int, bool]
)  # All valid

隨著 Python 3.12 泛型新語法的引入(由 PEP 695 引入),可以在編譯時期強制執行此規則。

type Alias[DefaultT = int, T] = tuple[DefaultT, T]  # SyntaxError: non-default TypeVars cannot follow ones with defaults

def generic_func[DefaultT = int, T](x: DefaultT, y: T) -> None: ...  # SyntaxError: non-default TypeVars cannot follow ones with defaults

class GenericClass[DefaultT = int, T]: ...  # SyntaxError: non-default TypeVars cannot follow ones with defaults

ParamSpec 預設值

ParamSpec 的預設值定義方式與 TypeVar 相同,但使用類型列表、省略符號字面值 “...” 或另一個作用域內的 ParamSpec(參見 作用域規則)。

DefaultP = ParamSpec("DefaultP", default=[str, int])

class Foo(Generic[DefaultP]): ...

reveal_type(Foo)                  # type is type[Foo[DefaultP = [str, int]]]
reveal_type(Foo())                # type is Foo[[str, int]]
reveal_type(Foo[[bool, bool]]())  # type is Foo[[bool, bool]]

TypeVarTuple 預設值

TypeVarTuple 的預設值定義方式與 TypeVar 相同,但使用已展開 (unpacked) 的類型元組,而非單一類型或另一個作用域內的 TypeVarTuple(參見 作用域規則)。

DefaultTs = TypeVarTuple("DefaultTs", default=Unpack[tuple[str, int]])

class Foo(Generic[*DefaultTs]): ...

reveal_type(Foo)               # type is type[Foo[DefaultTs = *tuple[str, int]]]
reveal_type(Foo())             # type is Foo[str, int]
reveal_type(Foo[int, bool]())  # type is Foo[int, bool]

使用另一個類型參數作為 default

這允許在泛型的類型參數缺失但已指定另一個類型參數時,重複使用某個值。

若要使用另一個類型參數作為預設值,該 default 與該類型參數必須為相同類型(例如 TypeVar 的預設值必須為 TypeVar,以此類推)。

這可用於 builtins.slice,其中 start 參數預設為 intstop 預設為 start 的類型,而 step 預設為 int | None

StartT = TypeVar("StartT", default=int)
StopT = TypeVar("StopT", default=StartT)
StepT = TypeVar("StepT", default=int | None)

class slice(Generic[StartT, StopT, StepT]): ...

reveal_type(slice)  # type is type[slice[StartT = int, StopT = StartT, StepT = int | None]]
reveal_type(slice())                        # type is slice[int, int, int | None]
reveal_type(slice[str]())                   # type is slice[str, str, int | None]
reveal_type(slice[str, bool, timedelta]())  # type is slice[str, bool, timedelta]

T2 = TypeVar("T2", default=DefaultStrT)

class Foo(Generic[DefaultStrT, T2]):
    def __init__(self, a: DefaultStrT, b: T2) -> None: ...

reveal_type(Foo(1, ""))  # type is Foo[int, str]
Foo[int](1, "")          # Invalid: Foo[int, str] cannot be assigned to self: Foo[int, int] in Foo.__init__
Foo[int]("", 1)          # Invalid: Foo[str, int] cannot be assigned to self: Foo[int, int] in Foo.__init__

當使用類型參數作為另一個類型參數的預設值時,適用下列規則(其中 T1T2 的預設值):

作用域規則

T1 必須在泛型的參數列表中早於 T2 使用。

T2 = TypeVar("T2", default=T1)

class Foo(Generic[T1, T2]): ...   # Valid
class Foo(Generic[T1]):
    class Bar(Generic[T2]): ...   # Valid

StartT = TypeVar("StartT", default="StopT")  # Swapped defaults around from previous example
StopT = TypeVar("StopT", default=int)
class slice(Generic[StartT, StopT, StepT]): ...
                  # ^^^^^^ Invalid: ordering does not allow StopT to be bound

不支援使用外部作用域的類型參數作為預設值。

邊界 (Bound) 規則

T1 的邊界 (bound) 必須是 T2 邊界的子類型。

T1 = TypeVar("T1", bound=int)
TypeVar("Ok", default=T1, bound=float)     # Valid
TypeVar("AlsoOk", default=T1, bound=int)   # Valid
TypeVar("Invalid", default=T1, bound=str)  # Invalid: int is not a subtype of str

約束 (Constraint) 規則

T2 的約束 (constraints) 必須是 T1 約束的超集。

T1 = TypeVar("T1", bound=int)
TypeVar("Invalid", float, str, default=T1)         # Invalid: upper bound int is incompatible with constraints float or str

T1 = TypeVar("T1", int, str)
TypeVar("AlsoOk", int, str, bool, default=T1)      # Valid
TypeVar("AlsoInvalid", bool, complex, default=T1)  # Invalid: {bool, complex} is not a superset of {int, str}

作為泛型參數的類型參數

當第一個參數依據前一節確定為在作用域內時,類型參數可作為 default 內部泛型的參數。

T = TypeVar("T")
ListDefaultT = TypeVar("ListDefaultT", default=list[T])

class Bar(Generic[T, ListDefaultT]):
    def __init__(self, x: T, y: ListDefaultT): ...

reveal_type(Bar)                    # type is type[Bar[T, ListDefaultT = list[T]]]
reveal_type(Bar[int])               # type is type[Bar[int, list[int]]]
reveal_type(Bar[int]())             # type is Bar[int, list[int]]
reveal_type(Bar[int, list[str]]())  # type is Bar[int, list[str]]
reveal_type(Bar[int, str]())        # type is Bar[int, str]

特化 (Specialisation) 規則

類型參數目前無法進一步訂閱 (subscripted)。若實作了 高階 TypeVar (Higher Kinded TypeVars),此情況可能會改變。

Generic TypeAlias

Generic TypeAlias 應能遵循一般的訂閱規則進行進一步訂閱。若類型參數擁有未被覆寫的預設值,則應視為已代入至 TypeAlias 中。不過,它仍可在後續進行特化。

class SomethingWithNoDefaults(Generic[T, T2]): ...

MyAlias: TypeAlias = SomethingWithNoDefaults[int, DefaultStrT]  # Valid
reveal_type(MyAlias)          # type is type[SomethingWithNoDefaults[int, DefaultStrT]]
reveal_type(MyAlias[bool]())  # type is SomethingWithNoDefaults[int, bool]

MyAlias[bool, int]  # Invalid: too many arguments passed to MyAlias

子類別化

具有預設值類型參數的 Generic 子類別之行為與 Generic TypeAlias 類似。即子類別可遵循一般訂閱規則進行進一步訂閱,未被覆寫的預設值會被代入,且帶有此類預設值的類型參數可在後續進一步特化。

class SubclassMe(Generic[T, DefaultStrT]):
    x: DefaultStrT

class Bar(SubclassMe[int, DefaultStrT]): ...
reveal_type(Bar)          # type is type[Bar[DefaultStrT = str]]
reveal_type(Bar())        # type is Bar[str]
reveal_type(Bar[bool]())  # type is Bar[bool]

class Foo(SubclassMe[float]): ...

reveal_type(Foo().x)  # type is str

Foo[str]  # Invalid: Foo cannot be further subscripted

class Baz(Generic[DefaultIntT, DefaultStrT]): ...

class Spam(Baz): ...
reveal_type(Spam())  # type is <subclass of Baz[int, str]>

同時使用 bounddefault

若同時傳入了 bounddefault,則 default 必須是 bound 的子類型。否則,類型檢查器應產生錯誤。

TypeVar("Ok", bound=float, default=int)     # Valid
TypeVar("Invalid", bound=str, default=int)  # Invalid: the bound and default are incompatible

限制條件

對於帶有約束 (constraints) 的 TypeVar,預設值必須為其中一個約束條件。即使預設值是某個約束的子類型,類型檢查器仍應產生錯誤。

TypeVar("Ok", float, str, default=float)     # Valid
TypeVar("Invalid", float, str, default=int)  # Invalid: expected one of float or str got int

函數預設值

在泛型函數中,當類型參數無法解析時,類型檢查器可能會使用其預設值。我們未明確定義此用法的語意,因為確保在每個類型參數無法解析的程式碼路徑中都回傳 default 可能難以實作。類型檢查器可以選擇禁止此情況,或實驗性地實作支援。

T = TypeVar('T', default=int)
def func(x: int | set[T]) -> T: ...
reveal_type(func(0))  # a type checker may reveal T's default of int here

TypeVarTuple 之後的預設值

緊跟在 TypeVarTuple 之後的 TypeVar 不允許擁有預設值,因為這會造成歧義:不確定型別參數應繫結至 TypeVarTuple 還是帶有預設值的 TypeVar

Ts = TypeVarTuple("Ts")
T = TypeVar("T", default=bool)

class Foo(Generic[Ts, T]): ...  # Type checker error

# Could be reasonably interpreted as either Ts = (int, str, float), T = bool
# or Ts = (int, str), T = float
Foo[int, str, float]

使用 Python 3.12 內建的泛型語法,此情況應引發 SyntaxError

然而,允許擁有預設值的 ParamSpec 跟隨在擁有預設值的 TypeVarTuple 之後,因為 ParamSpec 的型別參數與 TypeVarTuple 的型別參數之間不會產生歧義。

Ts = TypeVarTuple("Ts")
P = ParamSpec("P", default=[float, bool])

class Foo(Generic[Ts, P]): ...  # Valid

Foo[int, str]  # Ts = (int, str), P = [float, bool]
Foo[int, str, [bytes]]  # Ts = (int, str), P = [bytes]

子型別

類型參數預設值不會影響泛型類別的子型別規則。特別是,在考量類別是否相容於泛型協定 (generic protocol) 時,預設值是可以被忽略的。

TypeVarTuple 作為預設值

不支援將 TypeVarTuple 用作預設值,原因在於:

  • 作用域規則不允許使用外部作用域的類型參數。
  • PEP 646 所述,單一物件的類型參數列表中不能出現多個 TypeVarTuple

基於上述原因,目前沒有合適的位置可以將 TypeVarTuple 作為另一個 TypeVarTuple 的預設值。

繫結 (Binding) 規則

類型參數預設值應由屬性存取(包含呼叫與訂閱)進行繫結。

class Foo[T = int]:
    def meth(self) -> Self:
        return self

reveal_type(Foo.meth)  # type is (self: Foo[int]) -> Foo[int]

實作

在執行時期,這會涉及對 typing 模組的下列變更:

  • TypeVarParamSpecTypeVarTuple 類別應公開傳遞給 default 的類型。這將作為 __default__ 屬性提供;若未傳入參數,則為 None;若 default=None,則為 NoneType

對兩種 GenericAlias 都需要進行下列變更:

  • 確定訂閱所需的預設值之邏輯。
  • 理想情況下,確定訂閱(例如 Generic[T, DefaultT])是否有效之邏輯。

需要更新類型參數列表的語法以允許預設值;詳見下文。

執行時期變更的參考實作請見:https://github.com/Gobot1234/cpython/tree/pep-696

類型檢查器的參考實作請見:https://github.com/Gobot1234/mypy/tree/TypeVar-defaults

Pyright 目前已支援此功能。

語法變更

PEP 695 中加入的語法將會擴充,透過方括號內的 “=” 運算子指定類型參數預設值,如下所示:

# TypeVars
class Foo[T = str]: ...

# ParamSpecs
class Baz[**P = [int, str]]: ...

# TypeVarTuples
class Qux[*Ts = *tuple[int, bool]]: ...

# TypeAliases
type Foo[T, U = str] = Bar[T, U]
type Baz[**P = [int, str]] = Spam[**P]
type Qux[*Ts = *tuple[str]] = Ham[*Ts]
type Rab[U, T = str] = Bar[T, U]

與類型參數的邊界類似,預設值應為延遲評估 (lazily evaluated),並採用相同的作用域規則,以避免在其周圍不必要地使用引號。

此功能曾包含在 PEP 695 的初步草案中,但因範疇蔓延 (scope creep) 而被移除。

語法上將進行下列變更:

type_param:
    | a=NAME b=[type_param_bound] d=[type_param_default]
    | a=NAME c=[type_param_constraint] d=[type_param_default]
    | '*' a=NAME d=[type_param_default]
    | '**' a=NAME d=[type_param_default]

type_param_default:
    | '=' e=expression
    | '=' e=starred_expression

編譯器將強制執行以下規定:無預設值的類型參數不得跟隨在有預設值的類型參數之後,且帶有預設值的 TypeVar 不得緊跟在 TypeVarTuple 之後。

被拒絕的替代方案

允許類型參數預設值傳遞至 type.__new__**kwargs

T = TypeVar("T")

@dataclass
class Box(Generic[T], T=int):
    value: T | None = None

雖然這種方式更易於閱讀且遵循與 TypeVar 一元語法 (unary syntax) 相似的邏輯,但它無法向下相容,因為 T 可能已經被傳遞給後設類別 (metaclass)/父類別,或是在執行時期支援不繼承自 Generic 的類別。

理想情況下,若 PEP 637 未被拒絕,則以下形式是可以接受的:

T = TypeVar("T")

@dataclass
class Box(Generic[T = int]):
    value: T | None = None

允許非預設值跟隨在預設值之後

YieldT = TypeVar("YieldT", default=Any)
SendT = TypeVar("SendT", default=Any)
ReturnT = TypeVar("ReturnT")

class Coroutine(Generic[YieldT, SendT, ReturnT]): ...

Coroutine[int] == Coroutine[Any, Any, int]

允許非預設值跟隨在預設值之後,將能緩解從函數返回諸如 Coroutine 等型別時的問題(此類情況中,最常用的型別參數往往位於最後,即回傳值)。允許非預設值跟隨在預設值之後太過混亂且潛在歧義,即使僅允許上述兩種形式亦然。現在變更參數順序也會破壞許多程式碼庫。在大多數情況下,這也能透過 TypeAlias 解決。

Coro: TypeAlias = Coroutine[Any, Any, T]
Coro[int] == Coroutine[Any, Any, int]

default 隱式設定為 bound

在本 PEP 的早期版本中,若未傳遞 default 值,default 會隱式設定為 bound。這雖然方便,但可能會導致沒有預設值的類型參數跟隨在有預設值的類型參數之後。請考慮:

T = TypeVar("T", bound=int)  # default is implicitly int
U = TypeVar("U")

class Foo(Generic[T, U]):
    ...

# would expand to

T = TypeVar("T", bound=int, default=int)
U = TypeVar("U")

class Foo(Generic[T, U]):
    ...

對於少數依賴 Any 作為隱式預設值的程式碼,這會是一個破壞性的變更。

允許在函數簽章中使用帶有預設值的類型參數

本 PEP 的先前版本曾允許在函數簽章中使用帶有預設值的 TypeVarLike。此規則因 函數預設值 一節所述的原因被移除。若未來能新增取得類型參數執行時期值的方法,希望能在未來版本中重新加入。

允許在 default 中使用外部作用域的類型參數

此特性被視為過於小眾,不值得增加複雜度。若未來有任何場景確實需要此功能,可在未來的 PEP 中再行加入。

致謝

感謝以下人員對本 PEP 的回饋

Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morgan 與 Jakub Kuczys


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

最後修改日期:2024年9月3日 17:24:02 GMT