Python 官方文檔 PEP484(類型提示)的譯文,本人原創。 ...
英文原文: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)
- 註解的含義(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
類型(TheAny
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)
- 異常(Exceptions)
- typing 模塊(The typing Module)
- Python 2.7 和跨版本代碼的建議語法(Suggested syntax for Python 2.7 and straddling code)
- 未被接受的替代方案(Rejected Alternatives)
- 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)和特殊類型 Any
,Any
類型可與所有類型相容(即可以賦值給所有類型,也可以從所有類型賦值)。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 的未來版本中加入某些包含和排除項。
此外,以下結構也是可以用作類型註解的:None
、Any
、Union
、Tuple
、Callable
、用於構建由 typing
導出的類(如 Sequence
和 Dict
)的所有抽象基類(ABC)及其替代物(stand-in)、類型變數和類型別名。
以下章節介紹的特性當中,所有用於提供支持的新引入類型名(例如 Any
和 Union
)都在 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
支持把參數可能的類型限為一組固定值(註意:這裡的類型不能用類型變數實現參數化)。例如,可以定義某個類型變數只能是 str
和 bytes
。預設情況下,類型變數會覆蓋所有可能的類型。以下是一個約束類型變數範圍的示例:
from typing import TypeVar, Text
AnyStr = TypeVar('AnyStr', Text, bytes)
def concat(x: AnyStr, y: AnyStr) -> AnyStr:
return x + y
concat
函數對兩個 str
或兩個 bytes
參數都可以調用,但不能混合使用 str
和 bytes
參數。
只要存在約束條件,就至少應該有兩個,不允許只指定單個約束條件。
在類型變數的上下文中,類型變數約束類型的子類型應被視作顯式給出的對應基本類型。參見以下示例:
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個類型參數 T
的 LoggedVar
類。這也使得 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
請註意,p
和 q
的運行時類型(類)仍會保持為 Node
,Node[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
類似 Mapping
、Sequence
這種抽象集合類的泛型版本,以及 List
、Dict
、Set
、FrozenSet
這種內置類的泛型版本,都是不能被實例化的。但是,其具體的用戶自定義子類和具體具體集合類的泛型版本,就能被實例化了:
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=True
或 convariant=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
中的只讀集合類都將類型變數聲明為可協變的,比如 Mapping
和 Sequence
。可修改的集合類(如 MutableMapping
和 MutableSequence
)則聲明為不可變的(invariant)。協變類型的一個例子是 Generator
類型,其 send()
的參數類型是可協變的(參見下文)。
註意:協變和逆變並不是類型變數的特性,而是用該變數定義的泛型類的特性。可變性僅適用於泛型類型,泛型函數則沒有此特性。泛型函數只允許採用不帶 covariant
和 convariant
關鍵字參數的類型變數進行定義。例如以下示例就很不錯:
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
實現了對應的抽象基類(Number
、Complex
、Real
、Rational
和 Integral
)。關於這些抽象基類是存在一些爭議,但內置的具體實現的數值類 complex
、float
和 int
已得以廣泛應用(尤其是後兩個類:-)。
本 PEP 提出了一種簡單、快捷、幾乎也是高效的方案,用戶不必先寫 import numbers
語句再使用 umbers.Float
:只要註解為 float
類型,即可接受 int
類型的參數。類似地,註解為 complex
類型的參數,則可接受 float
或 int
類型。這種方案無法應對實現抽象基類或 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)的類型是所有 T
、T2
等類型的超級類型(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
是一個類型變數,具體請參閱相關章節。
任何其他的結構(如 Tuple
或 Callable
)均不能用作 Type
的參數。
此特性存在一些問題:比如若 new_user()
要調用 user_class()
,就意味著 User
的所有子類都必須在其構造函數的簽名中支持該調用。不過並不是只有 Type[]
才會如此,類方法也有類似的問題。類型檢查程式應該將違反這種假定的行為標記出來,但與所標明基類(如上例中的 User
)的構造函數簽名相符的構造函數,應該預設是允許調用的。如果程式中包含了比較複雜的或可擴展的類體系,也可以採用工廠類方法來作處理。本 PEP 的未來修訂版本可能會引入更好的方法來解決這些問題。
當 Type
帶有參數時,僅要求有一個參數。不帶中括弧的普通類型等效於 Type[Any]
,也即等效於 type
(Python 元類體系中的根類)。這種等效性也促成了其名稱 Type
,而沒有採用 Class
或 SubType
這種名稱,在討論此特性時這些名稱都被提出過,這有點類似 List
和 list
的關係。
關於 Type[Any]
(或 Type
、Type
)的行為,如果要訪問該類型變數的屬性,則只提供了 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
該模塊還為無法指定更精確類型的情況提供了泛型抽象基類 Awaitable
、AsyncIterable
和 AsyncIterator
:
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]
類型註釋應放在變數定義語句的最後一行,還可以緊挨著冒號放在 with
和 for
語句後面。
以下是with
和 for
語句的類型註解示例:
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 __