1. 程式人生 > >PEP 484 型別提示 -- Python官方文件譯文 [原創]

PEP 484 型別提示 -- Python官方文件譯文 [原創]

英文原文:https://www.python.org/dev/peps/pep-0484/
採集日期:2019-12-27

PEP 484 -- 型別提示(Type Hints)

PEP: 484
Title: Type Hints
Author: Guido van Rossum <guido at python.org>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, ?ukasz Langa <lukasz at python.org>
BDFL-Delegate: Mark Shannon
Discussions-To: Python-Dev <python-dev at python.org>

Status: Provisional
Type: Standards Track
Created: 29-Sep-2014
Python-Version: 3.5
Post-History: 16-Jan-2015、20-Mar-2015、17-Apr-2015、20-May-2015、22-May-2015
Resolution: https://mail.python.org/pipermail/python-dev/2015-May/140104.html

  • 摘要(Abstract)
  • 原由和目標(Rationale and goals)
    • 非目標(Non-goals)
  • 註解的含義(The meaning of annotations)
  • 型別定義的語法(Type definition syntax)
    • 可接受的型別提示(Acceptable type hints)
    • None 的用法(Using None)
    • 類型別名(Type aliases)
    • Callable
    • 泛型(Generics)
    • 使用者自定義的泛型型別(User-defined generic types)
    • 型別變數的作用域規則(Scoping rules for type variables)
    • 泛型類的例項化及型別清除(Instantiating generic classes and type erasure)
    • 用任意泛型型別作為基類(Arbitrary generic types as base classes))
    • 抽象泛型型別(Abstract generic types)
    • 帶有型別上界的型別變數(Type variables with an upper bound)
    • 協變和逆變(Covariance and contravariance)
    • 數值型別的繼承關係(The numeric tower)
    • 向前引用(Forward references)
    • Union 型別(Union types)
    • 用 Union 實現單例項型別的支援(Support for singleton types in unions)
    • Any 型別(The Any type)
    • NoReturn 型別(The NoReturn type)
    • 類物件的型別(The type of class objects)
    • 為例項和類方法加型別註解(Annotating instance and class methods)
    • 版本和平臺檢查(Version and platform checking)
    • 執行時檢查還是型別檢查?(Runtime or type checking?)
    • 可變引數列表和預設引數值(Arbitrary argument lists and default argument values)
    • 只採用位置引數(Positional-only arguments)
    • 註解生成器函式和協程(Annotating generator functions and coroutines)
  • 與函式註解其他用法的相容性(
    Compatibility with other uses of function annotations)
  • 型別註釋(Type comments)
  • 指定型別(Cast)
  • NewType 工具函式(NewType helper function)
  • 存根檔案(Stub Files)
    • 函式/方法過載(Function/method overloading)
    • 存根檔案的儲存和釋出(Storing and distributing stub files)
    • typeshed 庫(The Typeshed Repo)
  • 異常(Exceptions)
  • typing 模組(The typing Module)
  • Python 2.7 和跨版本程式碼的建議語法(Suggested syntax for Python 2.7 and straddling code)
  • 未被接受的替代方案(Rejected Alternatives)
    • 泛型引數該用什麼括號?(Which brackets for generic type parameters?)
    • 已存在的註解怎麼辦(What about existing uses of annotations?)
    • 前向宣告的問題(The problem of forward declarations)
    • 雙冒號(The double colon)
    • 其他一些新語法格式(Other forms of new syntax)
    • 其他的向下相容約定(Other backwards compatible conventions)
  • PEP 開發過程(PEP Development Process)
  • 致謝(Acknowledgements)
  • 參考文獻(References)
  • 版權(Copyright)

摘要(Abstract)

PEP 3107 已經引入了函式註解(annotation)的語法,但有意將語義(semantic)保留為未定義(undefined)。目前第三方的靜態型別分析應用工具已經足夠多了,社群人員採用標準用語和標準庫中的基線(baseline)工具就將獲益良多。

為了提供標準定義和工具,本 PEP 引入了一個臨時(provisional)模組,並且列出了一些不適用註解情形的約定。

請注意,即便註解符合本規範,本 PEP 依然明確不會妨礙註解的其他用法,也不要求(或禁止)對註解進行任何特殊處理。正如 PEP 333 對 Web 框架的約定,這只是為了能更好地相互合作。

比如以下這個簡單的函式,其引數和返回型別都在註解給出了宣告:

def greeting(name: str) -> str:
    return 'Hello ' + name

雖然在執行時通過常規的 __annotations__ 屬性可以訪問到上述註解,但執行時並不會進行型別檢查。本提案假定存在一個獨立的離線型別檢查程式,使用者可以自願對原始碼執行此檢查程式。這種型別檢查程式實質上就是一種非常強大的查錯工具(linter)。當然某些使用者是可以在執行時採用類似的檢查程式實現“契約式設計”或JIT優化,但這些工具尚未完全成熟。

本提案受到 mypy 的強烈啟發。例如,“整數序列”型別可以寫為 Sequence[int]。方括號表示無需向語言新增新的語法。上述示例用到了自定義型別 Sequence,是從純 Python 模組 typing 中匯入的。通過實現元類(metaclass)中的 __getitem__() 方法,Sequence[int] 表示法在執行時得以生效(但主要是對離線型別檢查程式有意義)。

型別系統支援型別組合(Union)、泛型型別(generic type)和特殊型別 AnyAny 型別可與所有型別相容(即可以賦值給所有型別,也可以從所有型別賦值)。Any 型別的特性取自漸進定型(gradual typing)的理念。漸進定型和全型別系統已在 PEP 483 中有所解釋。

在 PEP 482 中,還介紹了其他一些已借鑑或可比較的方案。

原由和目標(Rationale and Goals)

PEP 3107 已加入了為函式定義中的各個部分添加註解的支援。儘管沒有為註解定義什麼含義,但已經隱隱有了一個目標,即把註解用於型別提示 gvr-artima,在 PEP 3107 中這被列為第一個可能應用的場景。

本 PEP 旨在為型別註解提供一種標準語法,讓 Python 程式碼更加開放、更易於靜態分析和重構,提供一種潛在的執行時型別檢查方案,以及(或許在某些上下文中)能利用型別資訊生成程式碼。

在這些目標中,靜態分析是最重要的。包括了對 mypy 這類離線型別檢查程式的支援,以及可供 IDE 使用的程式碼自動補全和重構的標準表示法。

非目標(Non-goals)

雖然本提案的 typing 模組將包含一些用於執行時型別檢查的功能模組,特別是 get_type_hints() 函式,但必須開發第三方程式包才能實現特定的執行時型別檢查功能,比如使用裝飾器(decorator)或元類。至於如何利用型別提示進行效能優化,就留給讀者當作練習吧。

還有一點應該強調一下,Python 仍將保持為一種動態型別語言,並且按慣例作者從沒希望讓型別提示成為強制特性。

註解的含義(The meaning of annotations)

不帶註解的函式都應被視為其類​​型可能是最通用的,或者應被所有型別檢查器忽略的。具有 @no_type_check 裝飾器的函式應被視為不帶註解的。

建議但不強求被檢查函式的全部引數和返回型別都帶有註解。被檢查函式的引數和返回型別的預設註解為 Any。不過有一種例外情況,就是例項和類方法的第一個引數。如果未帶註解,則假定例項方法第一個引數的型別就是所在類(的型別),而類方法第一個引數的型別則為所在物件類(的型別)。例如,在類 A 中,例項方法第一個引數的型別隱含為 A。在類方法中,第一個引數的精確型別沒法用型別註解表示。

請注意,__init__ 的返回型別應該用 -> None 進行註解。原因比較微妙。如果假定__init__ 預設用 -> None 作為返回型別註解,那麼是否意味著無引數、不帶註解的__init__ 方法還需要做型別檢查?與其任其模稜兩可或引入異常,還不如規定 __init__ 應該帶有返回型別註解,預設表現與其他方法相同。

型別檢查程式應該對函式主體和所給註解的一致性進行檢查。這些註解還可以用於檢查其他被檢函式對該函式的呼叫是否正確。

型別檢查程式應該盡力推斷出儘可能多的資訊。最低要求是能夠處理內建裝飾器 @ property@ staticmethod@classmethod

型別定義的語法(Type definition syntax)

這裡的語法充分利用了 PEP 3107 風格的註解,並加入了以下章節介紹的一些擴充套件。型別提示的基本格式就是,把類名填入函式註解的位置:

def greeting(name: str) -> str:
    return 'Hello ' + name

以上表示引數 name 的預期型別為 str。類似地,預期的函式返回型別為 str

其型別是特定引數型別的子型別的表示式也被該引數接受。

可接受的型別提示(Acceptable type hints)

型別提示可以是內建類(含標準庫或第三方擴充套件模組中定義的)、抽象基類、types 模組中提供的型別和使用者自定義類(含標準庫或第三方庫中定義的)。

雖然註解通常是型別提示的最佳格式,但有時更適合用特殊註釋(comment)或在單獨釋出的存根檔案中表示。示例參見下文。

註解必須是有效的表示式,其求值過程不會讓定義函式時引發異常,不過向前引用(forward reference)的用法還請參見下文。

註解應儘量保持簡單,否則靜態分析工具可能無法對其進行解析。例如,動態計算出來的型別就不大能被理解。本項要求是有意含糊其辭的,根據討論結果可以在本 PEP 的未來版本中加入某些包含和排除項。

此外,以下結構也是可以用作型別註解的:NoneAnyUnionTupleCallable、用於構建由 typing 匯出的類(如 SequenceDict)的所有抽象基類(ABC)及其替代物(stand-in)、型別變數和類型別名。

以下章節介紹的特性當中,所有用於提供支援的新引入型別名(例如 AnyUnion)都在 typing 模組中給出了。

None 的用法(Using None)

None 用於型別提示中時,表示式 None 視作與 type(None) 等價。

類型別名(Type aliases)

定義類型別名很簡單,只要用變數賦值語句即可:

Url = str
def retry(url: Url, retry_count: int) -> None: ...

請注意,類型別名建議首字母用大寫,因為代表的是使用者自定義型別,使用者自定義的名稱(如使用者自定義的類)通常都用這種方式拼寫。

類型別名的複雜程度可以和註解中的型別提示一樣,型別註解可接受的內容在類型別名中均可接受:

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]

def inproduct(v: Vector[T]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Vector[T], scale: T) -> Vector[T]:
    return ((x * scale, y * scale) for x, y in v)
vec = []  # type: Vector[float]

以上語句等同於:

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)

def inproduct(v: Iterable[Tuple[T, T]]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Iterable[Tuple[T, T]], scale: T) -> Iterable[Tuple[T, T]]:
    return ((x * scale, y * scale) for x, y in v)
vec = []  # type: Iterable[Tuple[float, float]]

Callable

如果軟體框架需要返回特定簽名的回撥函式,則可以採用 Callable [[Arg1Type,Arg2Type] ReturnType] 的形式作為型別提示。例如:

from typing import Callable

def feeder(get_next_item: Callable[[], str]) -> None:
    # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body

在宣告返回 Callable 型別時也可以不指定呼叫簽名,只要用省略號(3個句點)代替引數列表即可:

def partial(func: Callable[..., str], *args) -> Callable[..., str]:
    # Body

請注意,省略號兩側並不帶方括號。在這種情況下,回撥函式的引數完全沒有限制,並且照樣可以使用帶關鍵字(keyword)的引數。

因為帶關鍵字引數的回撥函式並不常用,所以當前不支援指定 Callable 型別的帶關鍵字引數。同理,也不支援引數數量可變的回撥函式簽名。

因為 type.Callable 帶有雙重職能,用於替代 collections.abc.Callable,所以 isinstance(x, typing.Callable) 的實現與 isinstance(x, collections.abc.Callable) 相容。但是,isinstance(x, typing.Callable[...]) 是不受支援的。

泛型(Generics)

因為容器中的物件型別資訊無法以通用的方式做出靜態推斷,所以抽象基類已擴充套件為支援預約(subscription)特性,以標明容器內元素的預期型別。例如:

from typing import Mapping, Set

def notify_by_email(employees: Set[Employee], overrides: Mapping[str, str]) -> None: ...

利用 typing 模組中新提供的工廠函式 TypeVar,可以對泛型實現引數化。例如:

from typing import Sequence, TypeVar

T = TypeVar('T')      # Declare type variable

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

以上就是約定了返回值的型別與集合內的元素保持一致。

TypeVar() 表示式只能直接賦給某個變數(不允許用其組成其他表示式)。TypeVar() 的引數必須是一個字串,該字元等於分配給它的變數名。型別變數不允許重定義(redefine)。

TypeVar 支援把引數可能的型別限為一組固定值(注意:這裡的型別不能用型別變數實現引數化)。例如,可以定義某個型別變數只能是 strbytes。預設情況下,型別變數會覆蓋所有可能的型別。以下是一個約束型別變數範圍的示例:

from typing import TypeVar, Text

AnyStr = TypeVar('AnyStr', Text, bytes)

def concat(x: AnyStr, y: AnyStr) -> AnyStr:
    return x + y

concat 函式對兩個 str 或兩個 bytes 引數都可以呼叫,但不能混合使用 strbytes 引數。

只要存在約束條件,就至少應該有兩個,不允許只指定單個約束條件。

在型別變數的上下文中,型別變數約束型別的子型別應被視作顯式給出的對應基本型別。參見以下示例:

class MyStr(str): ...

x = concat(MyStr('apple'), MyStr('pie'))

上述呼叫是合法的,只是型別變數 AnyStr 將被設為 str 而非 MyStr。實際上,賦給 x 的返回值,其推斷型別也會是 str

此外,Any 對於所有型別變數而言都是合法值。參見以下示例:

def count_truthy(elements: List[Any]) -> int:
    return sum(1 for elem in elements if elem)

上述語句相當於省略了泛型註解,只寫了 elements: List

使用者自定義的泛型型別(User-defined generic types)

Generic 基類包含進來,即可將使用者自定義類定義為泛型類。例如:

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('{}: {}'.format(self.name, message))

作為基類的 Generic[T] 定義了帶有1個型別引數 TLoggedVar 類。這也使得 T 能在類的體內作為型別來使用。

Generic 基類會用到定義了 __getitem__ 的元類,以便 LoggedVar[t] 能作為型別來使用:

from typing import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

同一個泛型型別所賦的型別變數可以是任意多個,而且型別變數還可以用作約束條件。以下語句是合法的:

from typing import TypeVar, Generic
...

T = TypeVar('T')
S = TypeVar('S')

class Pair(Generic[T, S]):
    ...

Generic 的每個型別變數引數都必須唯一。因此,以下語句是非法的:

from typing import TypeVar, Generic
...

T = TypeVar('T')

class Pair(Generic[T, T]):   # INVALID
    ...

在比較簡單的場合,沒有必要用到 Generic[T],這時可以繼承其他的泛型類並指定型別變數引數:

from typing import TypeVar, Iterator

T = TypeVar('T')

class MyIter(Iterator[T]):
    ...

以上類的定義等價於:

class MyIter(Iterator[T], Generic[T]):

...

可以對 Generic 使用多重繼承:

from typing import TypeVar, Generic, Sized, Iterable, Container, Tuple

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
    ...

K = TypeVar('K')
V = TypeVar('V')

class MyMapping(Iterable[Tuple[K, V]],
                Container[Tuple[K, V]],
                Generic[K, V]):
    ...

如未指定型別引數,則泛型類的子類會假定引數的型別均為 Any。在以下示例中,MyIterable 就不是泛型類,而是隱式繼承自 Iterable[Any]

from typing import Iterable

class MyIterable(Iterable):  # Same as Iterable[Any]
    ...

泛型元類不受支援。

型別變數的作用域規則(Scoping rules for type variables)

型別變數遵循常規的名稱解析規則。但在靜態型別檢查的上下文中,存在一些特殊情況:

  • 泛型函式中用到的型別變數可以被推斷出來,以便在同一程式碼塊中表示不同的型別。例如:
from typing import TypeVar, Generic

T = TypeVar('T')

def fun_1(x: T) -> T: ...  # T here
def fun_2(x: T) -> T: ...  # and here could be different

fun_1(1)                   # This is OK, T is inferred to be int
fun_2('a')                 # This is also OK, now T is str
  • 當泛型類的方法中用到型別變數時,若該變數正好用作引數化類,那麼此型別變數一定是繫結不變的。例如:
from typing import TypeVar, Generic

T = TypeVar('T')

class MyClass(Generic[T]):
    def meth_1(self, x: T) -> T: ...  # T here
    def meth_2(self, x: T) -> T: ...  # and here are always the same

a = MyClass()  # type: MyClass[int]
a.meth_1(1)    # OK
a.meth_2('a')  # This is an error!
  • 如果某個方法中用到的型別變數與所有用於引數化類的變數都不相符,則會使得該方法成為返回型別為該型別變數的泛型函式:
T = TypeVar('T')
S = TypeVar('S')
class Foo(Generic[T]):
    def method(self, x: T, y: S) -> S:
        ...

x = Foo()               # type: Foo[int]
y = x.method(0, "abc")  # inferred type of y is str
  • 在泛型函式體內不應出現未繫結的型別變數,在類中除方法定義以外的地方也不應出現:
T = TypeVar('T')
S = TypeVar('S')

def a_fun(x: T) -> None:
    # this is OK
    y = []  # type: List[T]
    # but below is an error!
    y = []  # type: List[S]

class Bar(Generic[T]):
    # this is also an error
    an_attr = []  # type: List[S]

    def do_something(x: S) -> S:  # this is OK though
        ...
  • 如果泛型類的定義位於某泛型函式內部,則其不允許使用引數化該泛型函式的型別變數:
from typing import List

def a_fun(x: T) -> None:

    # This is OK
    a_list = []  # type: List[T]
    ...

    # This is however illegal
    class MyGeneric(Generic[T]):
        ...

巢狀的泛型類不能使用相同的型別變數。外部類的型別變數,作用域不會覆蓋內部類:

T = TypeVar('T')
S = TypeVar('S')

class Outer(Generic[T]):
    class Bad(Iterable[T]):       # Error
        ...
    class AlsoBad:
        x = None  # type: List[T] # Also an error

    class Inner(Iterable[S]):     # OK
        ...
    attr = None  # type: Inner[T] # Also OK

例項化通用類和型別清除(Instantiating generic classes and type erasure)

當然可以對使用者自定義的泛型類進行例項化。假定編寫了以下繼承自 Generic[T]Node 類:

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    ...

若要建立 Node 的例項,像普通類一樣呼叫 Node() 即可。在執行時,例項的型別(類)將會是 Node。但是對於型別檢查程式而言,會要求具備什麼型別呢?答案取決於呼叫時給出多少信hu息。如果建構函式(__init____new__)在其簽名中用了 T,且傳了相應的引數值,則會替換對應引數的型別。否則就假定為 Any。例如:

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    x = None  # type: T # Instance attribute (see below)
    def __init__(self, label: T = None) -> None:
        ...

x = Node('')  # Inferred type is Node[str]
y = Node(0)   # Inferred type is Node[int]
z = Node()    # Inferred type is Node[Any]

如果推斷的型別用了 [Any],但預期的型別更為具體,則可以用型別註釋(參見下文)強行指定變數的型別,例如:

# (continued from previous example)
a = Node()  # type: Node[int]
b = Node()  # type: Node[str]

或者,也可以例項化具體的型別,例如:

# (continued from previous example)
p = Node[int]()
q = Node[str]()
r = Node[int]('')  # Error
s = Node[str](0)   # Error

請注意,pq 的執行時型別(類)仍會保持為 NodeNode[int]Node[str] 是可相互區別的類物件,但通過例項化建立物件的執行時類不會記錄該區別。這種行為被稱作“型別清除(type erasure)”。在 Java、TypeScript 之類的支援泛型的語言中,這是一種常見做法。

通過泛型類(不論是否引數化)訪問屬性將會導致型別檢查失敗。在類定義體之外,無法對類的屬性進行賦值,它只能通過類的例項訪問,且該例項還不能帶有同名的例項屬性:

# (continued from previous example)
Node[int].x = 1  # Error
Node[int].x      # Error
Node.x = 1       # Error
Node.x           # Error
type(p).x        # Error
p.x              # Ok (evaluates to None)
Node[int]().x    # Ok (evaluates to None)
p.x = 1          # Ok, but assigning to instance attribute

類似 MappingSequence 這種抽象集合類的泛型版本,以及 ListDictSetFrozenSet 這種內建類的泛型版本,都是不能被例項化的。但是,其具體的使用者自定義子類和具體具體集合類的泛型版本,就能被例項化了:

data = DefaultDict[int, bytes]()

注意,請勿將靜態型別和執行時類混為一談。上述場合中,型別仍會被清除,並且以上表達式只是以下語句的簡寫形式:

data = collections.defaultdict()  # type: DefaultDict[int, bytes]

不建議在表示式中直接使用帶下標的類(例如 Node[int]),最好是採用類型別名(如 IntNode = Node [int])。首先,建立 Node[int] 這種帶下標的類會有一定的執行開銷。其次,使用類型別名的可讀性會更好。

用任意泛型型別作為基類(Arbitrary generic types as base classes)

Generic[T] 只能用作基類,它可不合適當作型別來使用。不過上述示例中的使用者自定義泛型型別(如 LinkedList[T]),以及內建的泛型型別和抽象基類(如 List[T]Iterable [T]),則既可以當作型別使用,也可以當作基類使用。例如,可以定義帶有特定型別引數的 Dict 子類:

from typing import Dict, List, Optional

class Node:
    ...

class SymbolTable(Dict[str, List[Node]]):
    def push(self, name: str, node: Node) -> None:
        self.setdefault(name, []).append(node)

    def pop(self, name: str) -> Node:
        return self[name].pop()

    def lookup(self, name: str) -> Optional[Node]:
        nodes = self.get(name)
        if nodes:
            return nodes[-1]
        return None

SymbolTable 既是 dict 的子類,也是 Dict[str,List [Node]] 的子型別。

如果某個泛型基類帶有型別變數作為型別實參,則會使其定義成為泛型類。比如可以定義一個既可迭代又是容器的 LinkedList 泛型類:

from typing import TypeVar, Iterable, Container

T = TypeVar('T')

class LinkedList(Iterable[T], Container[T]):
    ...

這樣 LinkedList[int] 就是一種合法的型別。注意在基類列表中可以多次使用 T,只要不在 Generic[...] 中多次使用同類型的變數 T 即可。

再來看看以下示例:

from typing import TypeVar, Mapping

T = TypeVar('T')

class MyDict(Mapping[str, T]):
    ...

以上情況下,MyDict 帶有單個引數 T

抽象泛型型別(Abstract generic types)

Generic 使用的元類是 abc.ABCMeta 的一個子類。通過包含抽象方法或屬性,泛型類可以成為抽象基類,並且泛型類也可以將抽象基類作為基類而不會出現元類衝突。

帶型別上界的型別變數(Type variables with an upper bound)

型別變數可以用 bound=<type> 指定型別上界(注意 <type> 本身不能由型別變數引數化)。這意味著,替換(顯式或隱式)型別變數的實際型別必須是上界型別的子型別。常見例子就是定義一個 Comparable 型別,這樣就足以捕獲最常見的錯誤了:

from typing import TypeVar

class Comparable(metaclass=ABCMeta):
    @abstractmethod
    def __lt__(self, other: Any) -> bool: ...
    ...  # __gt__ etc. as well

CT = TypeVar('CT', bound=Comparable)

def min(x: CT, y: CT) -> CT:
    if x < y:
        return x
    else:
        return y

min(1, 2)      # ok, return type int
min('x', 'y')  # ok, return type str

請注意,以上程式碼還不夠理想,比如 min('x', 1) 在執行時是非法的,但型別檢查程式只會推斷出返回型別是 Comparable。不幸的是,解決這個問題需要引入一個強大且複雜得多的概念,F有界多型性(F-bounded polymorphism)。後續可能還會再來討論這個問題。

型別上界不能與型別約束一起使用(如 AnyStr 中的用法,參見之前的示例),型別約束會使得推斷出的型別一定是約束型別之一,而型別上界則只要求實際型別是上界型別的子型別。

協變和逆變(Covariance and contravariance)

不妨假定有一個 Employee 類及其子類 Manager。假如有一個函式,引數用 List[Employee] 做了註解。那麼呼叫函式時是否該允許使用型別為 List[Manager] 的變數作引數呢?很多人都會不計後果地回答“是的,當然”。但是除非對該函數了解更多資訊,否則型別檢查程式應該拒絕此類呼叫:該函式可能會在 List 中加入 Employee 型別的例項,而這將與呼叫方的變數型別不符。

事實證明,以上這種引數是有逆變性的,直觀的回答(如果函式不對引數作出修改則沒問題!)是要求這種引數具備協變性。有關這些概念的詳細介紹,請參見 Wikipedia 和 PEP 483。這裡僅演示一下如何對型別檢查程式的行為進行控制。

預設情況下,所有泛型型別的變數均被視作不可變的,這意味著帶有 List[Employee] 這種型別註解的變數值必須與型別註解完全相符,不能是型別引數的子類或超類(上述示例中即為Employee)。

為了便於宣告可接受協變或逆變型別檢查的容器型別,型別變數可帶有關鍵字引數 covariant=Trueconvariant=True。兩者只能有一個。如果泛型型別帶有此類變數定義,則其變數會被相應視為具備協變或逆變性。按照約定,建議對帶有 covariant=True 定義的型別變數命名時採用 _co 結尾,而對於帶有 convariant=True 定義的型別變數則以 _contra 結尾來命名。

以下典型示例將會定義一個不可修改(immutable)或只讀的容器類:

from typing import TypeVar, Generic, Iterable, Iterator

T_co = TypeVar('T_co', covariant=True)

class ImmutableList(Generic[T_co]):
    def __init__(self, items: Iterable[T_co]) -> None: ...
    def __iter__(self) -> Iterator[T_co]: ...
    ...

class Employee: ...

class Manager(Employee): ...

def dump_employees(emps: ImmutableList[Employee]) -> None:
    for emp in emps:
        ...

mgrs = ImmutableList([Manager()])  # type: ImmutableList[Manager]
dump_employees(mgrs)  # OK

typing 中的只讀集合類都將型別變數宣告為可協變的,比如 MappingSequence。可修改的集合類(如 MutableMappingMutableSequence)則宣告為不可變的(invariant)。協變型別的一個例子是 Generator 型別,其 send() 的引數型別是可協變的(參見下文)。

注意:協變和逆變並不是型別變數的特性,而是用該變數定義的泛型類的特性。可變性僅適用於泛型型別,泛型函式則沒有此特性。泛型函式只允許採用不帶 covariantconvariant 關鍵字引數的型別變數進行定義。例如以下示例就很不錯:

from typing import TypeVar

class Employee: ...

class Manager(Employee): ...

E = TypeVar('E', bound=Employee)

def dump_employee(e: E) -> None: ...

dump_employee(Manager())  # OK

而以下寫法是不可以的:

B_co = TypeVar('B_co', covariant=True)

def bad_func(x: B_co) -> B_co:  # Flagged as error by a type checker
    ...

數值型別的繼承關係(The numeric tower)

PEP 3141 定義了 Python 的數值型別層級關係(numeric tower),並且 stdlib 的模組 numbers 實現了對應的抽象基類(NumberComplexRealRationalIntegral)。關於這些抽象基類是存在一些爭議,但內建的具體實現的數值類 complexfloatint 已得以廣泛應用(尤其是後兩個類:-)。

本 PEP 提出了一種簡單、快捷、幾乎也是高效的方案,使用者不必先寫 import numbers 語句再使用 umbers.Float:只要註解為 float 型別,即可接受 int 型別的引數。類似地,註解為 complex 型別的引數,則可接受 floatint 型別。這種方案無法應對實現抽象基類或 Fractions.Fraction 類的類,但可以相信那些使用者場景極為罕見。

向前引用(Forward references)

當型別提示包含尚未定義的名稱時,未定義名稱可以先表示為字串字面量(literal),稍後再作解析。

在定義容器類時,通常就會發生這種情況,這時在某些方法的簽名中會出現將要定義的類。例如,以下程式碼(簡單的二叉樹實現的開始部分)將無法生效:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

為了解決問題,可以寫為:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

此字串字面量應包含一個合法的 Python 表示式,即 compile(lit, '', 'eval') 應該是有效的程式碼物件,並且在模組全部載入完成後對其求值應該不會出錯。對該表示式求值時所處的區域性和全域性名稱空間應與對同一函式的預設引數求值時的名稱空間相同。

此外,該表示式應可被解析為合法的型別提示,即受限於“可接受的型別提示”一節中的規則約束。

允許將字串字面量用作型別提示的一部分,例如:

class Tree:
    ...
    def leaves(self) -> List['Tree']:
        ...

向前引用的常見應用場景是簽名需要用到 Django 模型。通常,每個模型都存放在單獨的檔案中,並且模型有一些方法的引數型別會涉及到其他的模型。因為 Python 存在迴圈匯入(circular import)處理機制,往往不可能直接匯入所有要用到的模型:

# File models/a.py
from models.b import B
class A(Model):
    def foo(self, b: B): ...

# File models/b.py
from models.a import A
class B(Model):
    def bar(self, a: A): ...

# File main.py
from models.a import A
from models.b import B

假定先匯入了 main,則 models/b.py 的 from models.a import A 一行將會執行失敗,報錯 ImportError,因為在 a 定義類 A 之前就打算從 model/a.py 匯入它。解決辦法是換成只匯入模組,並通過_module_._class_名引用 models:

# File models/a.py
from models import b
class A(Model):
    def foo(self, b: 'b.B'): ...

# File models/b.py
from models import a
class B(Model):
    def bar(self, a: 'a.A'): ...

# File main.py
from models.a import A
from models.b import B

Union 型別(Union types)

因為一個引數可接受數量有限的幾種預期型別是常見需求,所以系統新提供了一個特殊的工廠類,名為 Union。例如:

from typing import Union

def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
    if isinstance(e, Employee):
        e = [e]
    ...

Union[T1, T2, ...] 生成(factor)的型別是所有 TT2 等型別的超級型別(supertype),因此只要是這些型別之一的值就可被 Union[T1, T2, ...] 註解的引數所接受。

Union 型別的一種常見情況是 Optional 型別。除非函式定義中提供了預設值 None,否則 None 預設是不能當任意型別的值使用。例如:

def handle_employee(e: Union[Employee, None]) -> None: ...

Union[T1,None] 可以簡寫為 Optional[T1],比如以上語句等同於:

from typing import Optional

def handle_employee(e: Optional[Employee]) -> None: ...

本 PEP 以前允許型別檢查程式在預設值為 None 時假定採用 Optional 型別,如下所示:

def handle_employee(e: Employee = None): ...

將被視為等效於:

def handle_employee(e: Optional[Employee] = None) -> None: ...

現在不再推薦這種做法了。型別檢查程式應該與時俱進,將需要 Optional 型別的地方明確指出來。

用 Union 實現單例項型別的支援(Support for singleton types in unions)

單例項通常用於標記某些特殊條件,特別是 None 也是合法變數值的情況下。例如:

_empty = object()

def func(x=_empty):
    if x is _empty:  # default argument value
        return 0
    elif x is None:  # argument was provided and it's None
        return 1
    else:
        return x * 2

為了在這種情況下允許精確設定型別,使用者應結合使用 Union 型別和標準庫提供的 enum.Enum 類,這樣就能靜態捕獲型別錯誤了:

from typing import Union
from enum import Enum

class Empty(Enum):
    token = 0
_empty = Empty.token

def func(x: Union[int, None, Empty] = _empty) -> int:

    boom = x * 42  # This fails type check

    if x is _empty:
        return 0
    elif x is None:
        return 1
    else:  # At this point typechecker knows that x can only have type int
        return x * 2

因為 Enum 的子類無法被繼承,所以在上述示例的所有分支中都能靜態推斷出變數 x 的型別。需要多種單例物件的情形也同樣適用,可以使用包含多個值的列舉:

class Reason(Enum):
    timeout = 1
    error = 2

def process(response: Union[str, Reason] = '') -> str:
    if response is Reason.timeout:
        return 'TIMEOUT'
    elif response is Reason.error:
        return 'ERROR'
    else:
        # response can be only str, all other possible values exhausted
        return 'PROCESSED: ' + response

Any 型別(The Any type)

Any 是一種特殊的型別。每種型別都與 Any 相符。可以將其視為包含所有值和所有方法的型別。請注意,Any 和內建的型別物件完全不同。

當某個值的型別為 object 時,型別檢查程式將拒絕幾乎所有對其進行的操作,將其賦給型別更具體的變數(或將其用作返回值)將是一種型別錯誤。反之,當值的型別為Any時,型別檢查程式將允許對其執行的所有操作,並且
Any 型別的值可以賦給型別更具體(constrained)的變數(或用作返回值)。

不帶型別註解的函式引數假定就是用 Any 作為註解的。如果用了泛型型別但又未指定型別引數,則也假定引數型別為 Any

from typing import Mapping

def use_map(m: Mapping) -> None:  # Same as Mapping[Any, Any]
    ...

上述規則也適用於 Tuple,在型別註解的上下文中,Tuple 等效於 Tuple[Any, ...],即等效於 tuple。同樣,型別註解中的 Callable 等效於 Callable[[...], Any],即等效於 collections.abc.Callable

from typing import Tuple, List, Callable

def check_args(args: Tuple) -> bool:
    ...

check_args(())           # OK
check_args((42, 'abc'))  # Also OK
check_args(3.14)         # Flagged as error by a type checker

# A list of arbitrary callables is accepted by this function
def apply_callbacks(cbs: List[Callable]) -> None:
    ...

NoReturn 型別(The NoReturn type)

typing 模組提供了一種特殊的型別 NoReturn,用於註解一定不會正常返回的函式。例如一個將無條件引發異常的函式:

from typing import NoReturn

def stop() -> NoReturn:
    raise RuntimeError('no way')

型別註解 NoReturn 用於 sys.exit 之類的函式。靜態型別檢查程式將會確保返回型別註解為 NoReturn 的函式確實不會隱式或顯式地返回:

import sys
from typing import NoReturn

  def f(x: int) -> NoReturn:  # Error, f(0) implicitly returns None
      if x != 0:
          sys.exit(1)

型別檢查程式還會識別出調用此類函式後面的程式碼是否可達,並採取相應動作:

# continue from first example
def g(x: int) -> int:
    if x > 0:
        return x
    stop()
    return 'whatever works'  # Error might be not reported by some checkers
                             # that ignore errors in unreachable blocks

NoReturn 型別僅可用於函式的返回型別註解,出現在其他位置則被認為是錯誤:

from typing import List, NoReturn

# All of the following are errors
def bad1(x: NoReturn) -> int:
    ...
bad2 = None  # type: NoReturn
def bad3() -> List[NoReturn]:
    ...

類物件的型別(The type of class objects)

有時會涉及到類物件,特別是從某個類繼承而來的類物件。類物件可被寫為 Type[C],這裡的 C 是一個類。為了清楚起見,C 在用作型別註解時指的是類 C 的例項,Type[C] 指的是 C 的子類。這類似於物件和型別之間的區別。

例如,假設有以下類:

class User: ...  # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...

假設有一個函式,如果傳一個類物件進去,就會創建出該類的一個例項:

def new_user(user_class):
    user = user_class()
    # (Here we could write the user object to a database)
    return user

若不用 Type[],能給 new_user() 加上的最好的型別註解將會是:

def new_user(user_class: type) -> User:
    ...

但採用 Type[] 和帶上界的型別變數,就可以註解得更好:

U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
    ...

現在,若用 User 的某個子類做引數呼叫 new_user(),型別檢查程式將能推斷出結果的正確型別:

joe = new_user(BasicUser)  # Inferred type is BasicUser

Type[C] 對應的值必須是型別為 C 的子型別的類物件實體,而不是某個具體的型別。換句話說,在上述示例中,new_user(Union[BasicUser, ProUser]) 之類的呼叫將被型別檢查程式拒絕(並且會執行失敗,因為 union 無法例項化)。

請注意,用類的 union 作 Type[] 的引數是合法的,如下所示:

def new_non_team_user(user_class: Type[Union[BasicUser, ProUser]]):
    user = new_user(user_class)
    ...

但是,在執行時上例中傳入的實際引數仍必須是具體的類物件:

new_non_team_user(ProUser)  # OK
new_non_team_user(TeamUser)  # Disallowed by type checker

Type[Any] 也是支援的,含義參見下文。

為類方法的第一個引數標註型別註解時,允許採用 Type[T],這裡的 T 是一個型別變數,具體請參閱相關章節。

任何其他的結構(如 TupleCallable)均不能用作 Type 的引數。

此特性存在一些問題:比如若 new_user() 要呼叫 user_class(),就意味著 User 的所有子類都必須在其建構函式的簽名中支援該呼叫。不過並不是只有 Type[] 才會如此,類方法也有類似的問題。型別檢查程式應該將違反這種假定的行為標記出來,但與所標明基類(如上例中的 User)的建構函式簽名相符的建構函式,應該預設是允許呼叫的。如果程式中包含了比較複雜的或可擴充套件的類體系,也可以採用工廠類方法來作處理。本 PEP 的未來修訂版本可能會引入更好的方法來解決這些問題。

Type 帶有引數時,僅要求有一個引數。不帶中括號的普通型別等效於 Type[Any],也即等效於 type(Python 元類體系中的根類)。這種等效性也促成了其名稱 Type,而沒有采用 ClassSubType 這種名稱,在討論此特性時這些名稱都被提出過,這有點類似 Listlist 的關係。

關於 Type[Any](或 TypeType)的行為,如果要訪問該型別變數的屬性,則只提供了 type 定義的屬性和方法(如 __repr__()__mro__)。此類變數可以用任意引數進行呼叫,返回型別則為 Any

Type 的引數是協變的,因為 Type[Derived]Type[Base] 的子型別:

def new_pro_user(pro_user_class: Type[ProUser]):
    user = new_user(pro_user_class)  # OK
    ...

為例項和類方法加型別註解(Annotating instance and class methods)

大多數情況下,類和例項方法的第一個引數不需要加型別註解,對例項方法而言假定它的型別就是所在類(的型別),對類方法而言它則是所在類物件對應的型別對​​象(的型別)。另外,例項方法的第一個引數加型別註解時可以帶有一個型別變數。這時返回型別可以採用相同的型別變數,從而使該方法成為泛型函式。例如:

T = TypeVar('T', bound='Copyable')
class Copyable:
    def copy(self: T) -> T:
        # return a copy of self

class C(Copyable): ...
c = C()
c2 = c.copy()  # type here should be C

同樣,可以對類方法第一個引數的型別註解中使用 Type[]

T = TypeVar('T', bound='C')
class C:
    @classmethod
    def factory(cls: Type[T]) -> T:
        # make a new instance of cls

class D(C): ...
d = D.factory()  # type here should be D

請注意,某些型別檢查程式可能對以上用法施加限制,比如要求所用型別變數具備合適的型別上界(參見示例)。

版本和平臺檢查(Version and platform checking)

型別檢查程式應該能理解簡單的版本和平臺檢查語句,例如:

import sys

if sys.version_info[0] >= 3:
    # Python 3 specific definitions
else:
    # Python 2 specific definitions

if sys.platform == 'win32':
    # Windows specific definitions
else:
    # Posix specific definitions

請別指望型別檢查程式能理解諸如 "".join(reversed(sys.platform)) == "xunil" 這種晦澀語句。

執行時檢查還是型別檢查?(Runtime or type checking?)

有時候,有些程式碼必須由型別檢查程式(或其他靜態分析工具)進行檢查,而不應拿去執行。typing 模組為這種情況定義了一個常量 TYPE_CHECKING,在型別檢查(或其他靜態分析)期間視其為 True,在執行時視其為 False。例如:

import typing

if typing.TYPE_CHECKING:
    import expensive_mod

def a_func(arg: 'expensive_mod.SomeClass') -> None:
    a_var = arg  # type: expensive_mod.SomeClass
    ...

注意,這裡的型別註解必須用引號引起來,使其成為“向前引用”,以便向直譯器隱藏 expensive_mod 引用。在 # type 註釋中無需加引號。

這種做法對於處理迴圈匯入也會比較有用。

可變引數列表和預設引數值(Arbitrary argument lists and default argument values)

可變引數列表也可以加註型別註解,以下定義是可行的:

def foo(*args: str, **kwds: int): ...

這表示以下函式呼叫的引數型別都是合法的:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

foo 函式體中,變數 args 的型別被推導為 Tuple[str, ...],變數 kwds 的型別被推導為 Dict [str, int]

在存根(stub)檔案中,將引數宣告為帶有預設值,但不指定實際的預設值,這會很有用。例如:

def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...

預設值應該是如何的?""b""None 都不符合型別約束。

這時可將預設值指定為省略號,其實就是以上示例。

只採用位置引數(Positional-only arguments)

有一些函式被設計成只能按位置接收引數,並希望呼叫者不要使用引數名稱,不通過關鍵字給出引數。名稱以__開頭的引數均被假定為只按位置訪問,除非同時以__結尾:

def quux(__x: int, __y__: int = 0) -> None: ...

quux(3, __y__=1)  # This call is fine.

quux(__x=3)  # This call is an error.

為生成器函式和協程加型別註解(Annotating generator functions and coroutines)

生成器函式的返回型別可以用 type.py 模組提供的泛型 Generator[yield_type, send_type, return_type] 進行型別註解:

def echo_round() -> Generator[int, float, str]:
    res = yield
    while res:
        res = yield round(res)
    return 'OK'

PEP 492 中引入的協程(coroutine)可用與普通函式相同的語法進行型別註解。但是,返回型別的型別註解對應的是 await 表示式的型別,而不是協程的型別:

async def spam(ignored: int) -> str:
    return 'spam'

async def foo() -> None:
    bar = await spam(42)  # type: str

type.py 模組提供了一個抽象基類 collections.abc.Coroutine 的泛型版本,以支援可非同步呼叫(awaitable)特性,同時支援 send()throw() 方法。型別變數定義及其順序與 Generator 的相對應,即 Coroutine[T_co, T_contra, V_co],例如:

from typing import List, Coroutine
c = None  # type: Coroutine[List[str], str, int]
...
x = c.send('hi')  # type: List[str]
async def bar() -> None:
    x = await c  # type: int

該模組還為無法指定更精確型別的情況提供了泛型抽象基類 AwaitableAsyncIterableAsyncIterator

def op() -> typing.Awaitable[str]:
    if cond:
        return spam(42)
    else:
        return asyncio.Future(...)

與函式註解其他用法的相容性(Compatibility with other uses of function annotations)

有一些函式註解的使用場景,與型別提示是不相容的。這些用法可能會引起靜態型別檢查程式的混亂。但因為型別提示的註解在執行時不起作用(計算註解表示式、將註解儲存在函式物件的 __annotations__ 屬性中除外),所以不會讓程式報錯,只是可能會讓型別檢查程式發出虛報警告或錯誤。

如果要讓某部分程式不受型別提示的影響,可以用以下一種或幾種方法進行標記:

  • # type: ignore 加註釋(comment);
  • 為類或函式加上 @no_type_check 裝飾符(decorator);
  • 為自定義類或函式裝飾符加上 @no_type_check_decorator 標記。

更多詳情,請參見後續章節。

為了最大程度與離線型別檢查過程保持相容,將依賴於型別註解的介面改成其他機制(例如裝飾器)可能比較合適些。不過這在 Python 3.5 中沒什麼關係。更多討論請參見後續的“未被採納的其他方案”。

型別註釋(Type comments)

本 PEP 並未將變數明確標為某型別提供一等語法支援。為了有助於在複雜情況下進行型別推斷,可以採用以下格式的註釋:

x = []                # type: List[Employee]
x, y, z = [], [], []  # type: List[int], List[int], List[str]
x, y, z = [], [], []  # type: (List[int], List[int], List[str])
a, b, *c = range(5)   # type: float, float, List[float]
x = [1, 2]            # type: List[int]

型別註釋應放在變數定義語句的最後一行,還可以緊挨著冒號放在 withfor 語句後面。

以下是withfor 語句的型別註解示例:

with frobnicate() as foo:  # type: int
    # Here foo is an int
    ...

for x, y in points:  # type: float, float
    # Here x and y are floats
    ...

在存根(stub)檔案中,只宣告變數的存在但不給出初值可能會比較有用。這用 PEP 526 的變數註解語法即可實現:

from typing import IO

stream: IO[str]

上述語法在所有版本的 Python 的存根檔案中均可接受。但在 Python 3.5 以前版本的非存根檔案程式碼中,存在一種特殊情況:

from typing import IO

stream = None  # type: IO[str]

儘管 None 與給定型別不符,型別檢查程式不應對上述語句報錯,也不應將型別推斷結果更改為 Optional[...](雖然規則要求對註解預設值為 None 的引數如此操作)。這裡假定將由其他程式碼保證賦予變數型別合適的值,並且所有呼叫都可假定該變數具有給定型別。

註釋 # type: ignore 應該放在錯誤資訊所在行上:

import http.client
errors = {
    'not_found': http.client.NOT_FOUND  # type: ignore
}

如果註釋 # type: ignore 位於檔案的開頭、單獨佔一行、在所有文件字串(docstring)、import 語句或其他可執行程式碼之前,則會讓檔案中所有錯誤都不報錯。空行和其他註釋(如 shebang 程式碼行和編碼 cookie)可以出現在 # type: ignore 之前。

某些時候,型別註釋可能需要與查錯(lint)工具或其他註釋同處一行。此時型別註釋應位於其他註釋和 lint 標記之前:

# type: ignore # <comment or other marker>

如果大多時候型別提示能被證明有用,那麼將來版本的 Python 可能會為 typing 變數提供語法。

更新:該語法已通過 PEP 526 在 Python 3.6 加入。

指定型別(Cast)

偶爾,型別檢查程式可能需要另一種型別提示:程式設計師可能知道,某個表示式的型別比型別檢查程式能夠推斷出來的更為準確。例如:

from typing import List, cast

def find_first_str(a: List[object]) -> str:
    index = next(i for i, x in enumerate(a) if isinstance(x, str))
    # We only get here if there's at least one string in a
    return cast(str, a[index])

某些型別檢查程式可能無法推斷出 a[index] 的型別為 str,而只能推斷出是個物件或 Any,但大家都知道(如果程式碼能夠執行到該點)它必須是個字串。ast(t, x) 呼叫會通知型別檢查程式,確信 x 的型別就是 t。在執行時,cast 始終會原封不動地返回表示式,不作型別檢查,也不對值作任何轉換或強制轉換。

cast 與型別註釋(參見上一節)不同。用了型別註釋,型別檢查程式仍應驗證推斷出的型別是否與宣告的型別一致。若用了 cast ,型別檢查程式就會完全信任程式設計師。cast 還可以在表示式中使用,而型別註釋則只能在賦值時使用。

NewType 工具函式(NewType helper function)

還有些時候,為了避免邏輯錯誤,程式設計師可能會建立簡單的類。例如:

class UserId(int):
    pass

get_by_user_id(user_id: UserId):
    ...

但建立類會引入執行時的開銷。為了避免這種情況,typeing.py 提供了一個工具函式 NewType,該函式能夠建立執行開銷幾乎為零的唯一簡單型別。對於靜態型別檢查程式而言,Derived = NewType('Derived', Base) 大致等同於以下定義:

class Derived(Base):
    def __init__(self, _x: Base) -> None:
        ...

在執行時,NewType('Derived', Base) 將返回一個偽(dummy)函式,該偽函式只是簡單地將引數返回。型別檢查程式在用到 UserId 時要求由 int 顯式轉換(cast)而來,而用到 int 時要求由 UserId 顯式轉換而來。例如:

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    ...

UserId('user')          # Fails type check

name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK

num = UserId(5) + 1     # type: int

NewType 只能接受兩個引數:新的唯一型別名稱、基類。後者應為合法的類(即不是 Union 這種型別結構),或者是通過呼叫 NewType 建立的其他唯一型別。NewType 返回的函式僅接受一個引數,這等同於僅支援一個建構函式,建構函式只能接受一個基類例項作引數(參見上文)。例如:

class PacketId:
    def __init__(self, major: int, minor: int) -> None:
        self._major = major
        self._minor = minor

TcpPacketId = NewType('TcpPacketId', PacketId)

packet = PacketId(100, 100)
tcp_packet = TcpPacketId(packet)  # OK

tcp_packet = TcpPacketId(127, 0)  # Fails in type checker and at runtime

NewType('Derived', Base) 進行 isinstanceissubclass 和派生子類的操作都會失敗,因為函式物件不支援這些操作。

存根檔案(Stub Files)

存根檔案是包含型別提示資訊的檔案,這些提示資訊僅供型別檢查程式使用,而在執行時則不會用到。存根檔案有以下幾種使用場景:

  • 擴充套件模組
  • 作者尚未新增型別提示的第三方模組
  • 尚未編寫型別提示的標準庫模組
  • 必須與 Python 2和3相容的模組
  • 因為其他目的使用型別註解的模組

存根檔案的語法與常規 Python 模組相同。typing 模組中有一項特性在存根檔案中會有所不同:@overload 裝飾器,後續將會介紹。