英文原文:https://www.python.org/dev/peps/pep-0484/
采集日期:2019-12-27
PEP 484 -- 類型提示(Type Hints)
PEP: 484
Title: Type Hints
Author: Guido van Rossum
BDFL-Delegate: Mark Shannon
Discussions-To: Python-Dev
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 __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)
進行 isinstance
、issubclass
和派生子類的操作都會失敗,因為函數對象不支持這些操作。
存根文件(Stub Files)
存根文件是包含類型提示信息的文件,這些提示信息僅供類型檢查程序使用,而在運行時則不會用到。存根文件有以下幾種使用場景:
- 擴展模塊
- 作者尚未添加類型提示的第三方模塊
- 尚未編寫類型提示的標准庫模塊
- 必須與 Python 2和3兼容的模塊
- 因為其他目的使用類型注解的模塊
存根文件的語法與常規 Python 模塊相同。typing
模塊中有一項特性在存根文件中會有所不同:@overload
裝飾器,后續將會介紹。
類型檢查程序只應檢查存根文件中的函數簽名,建議存根文件中的函數體只寫一個省略號(...)。
類型檢查程序應可配置存根文件的搜索路徑。如果能找到存根文件,類型檢查程序就不應再去讀取對應的“實際”代碼模塊了。
盡管存根文件在語法上是合法的 Python 模塊,但他們采用 .pyi
作為擴展名,這樣就能將存根文件與對應的實際模塊在同一目錄中加以管理了。這也強調了以下觀念:存根文件不該具備任何運行時的行為。
存根文件的一些其他注意事項:
- 除非采用
import ... as ...
或等效的from ... import ... as ...
形式,否則導入到存根文件中的模塊和變量均不視作能從存根文件導出(export)的。 - 但上一條有一個例外,所有用
from ... import *
導入存根文件的對象均被視為導出屬性。這樣從某個模塊中導出所有對象就更加容易了,此模塊的內容可能因不同的 Python 版本而各不相同。 - 正如普通的 Python 文件導入 一樣,子模塊在導入之后會自動成為其父模塊的導出屬性。比如假設
spam
包帶有以下目錄結構:
spam/
__init__.pyi
ham.pyi
這里 __init__.pyi
中包含一條 from . import ham
或 from .ham import Ham
語句,則 ham
就成為 spam
的一條導出屬性。
- 存根文件可以不完整。為了讓類型檢查程序意識到這一點,存根文件可以包含以下代碼:
def __getattr__(name) -> Any: ...
因此,所有未在存根文件中定義的標識符都假定為 Any
類型。
函數/方法重載(Function/method overloading)
@overload
裝飾器可用於描述那些支持多種類型組合參數的函數和方法。內置模塊和類型中經常使用這種模式。例如,bytes
類型的 __getitem__()
方法可以描述如下:
from typing import overload
class bytes:
...
@overload
def __getitem__(self, i: int) -> int: ...
@overload
def __getitem__(self, s: slice) -> bytes: ...
這種描述方式能比 union(無法表達參數和返回類型之間的關系)更為精確:
from typing import Union
class bytes:
...
def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]: ...
@overload
的另一個用武之地是內置 map()
函數的類型,該函數的參數數量不定,具體取決於 callable 對象的類型:
from typing import Callable, Iterable, Iterator, Tuple, TypeVar, overload
T1 = TypeVar('T1')
T2 = TypeVar('T2)
S = TypeVar('S')
@overload
def map(func: Callable[[T1], S], iter1: Iterable[T1]) -> Iterator[S]: ...
@overload
def map(func: Callable[[T1, T2], S],
iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterator[S]: ...
# ... and we could add more items to support more than two iterables
請注意,還可以輕松加入參數項以支持 map(None, ...)
:
@overload
def map(func: None, iter1: Iterable[T1]) -> Iterable[T1]: ...
@overload
def map(func: None,
iter1: Iterable[T1],
iter2: Iterable[T2]) -> Iterable[Tuple[T1, T2]]: ...
@overload
裝飾器的上述用法同樣適用於存根文件。在常規模塊中,一串@overload
裝飾的定義之后必須緊跟一個非 @overload
定義(對於同一函數/方法而言)。 @overload
裝飾的定義僅對類型檢查程序有用,因為他們將被非 @overload
裝飾的定義覆蓋掉,非 @overload
裝飾的定義在運行時有用而應被類型檢查程序忽略。在運行時,直接調用 @overload
裝飾過的函數將會引發 NotImplementedError
。下面是一個非存根方式重載的示例,它無法簡單地用 union 或類型變量進行表達:
@overload
def utf8(value: None) -> None:
pass
@overload
def utf8(value: bytes) -> bytes:
pass
@overload
def utf8(value: unicode) -> bytes:
pass
def utf8(value):
<actual implementation>
注意:雖然用上述語法有可能實現多重分派(multiple dispatch),但這種實現需要用到 sys._getframe()
,這是令人生厭的。同樣,設計並實現高效的多重分派機制是很難的,這就是為什么以前的嘗試因 functools.singledispatch()
而被放棄的原因。(請參閱 PEP 443,尤其是其“替代方案”部分。)將來,可能會有令人滿意的多重分派設計,但是設計方案不應受到上述重載語法的限制,此重載語法是為存根文件中的類型提示定義的。也有可能這兩種特性會彼此獨立地研發(因為類型檢查程序中的重載與運行時的多重派發具有不同的應用場景和要求,比如后者不太可能支持泛型)。
通常可以用受限的 TypeVar
類型來代替 @overload
裝飾器。例如,以下存根文件中的 concat1
和 concat2
定義是等價的:
from typing import TypeVar, Text
AnyStr = TypeVar('AnyStr', Text, bytes)
def concat1(x: AnyStr, y: AnyStr) -> AnyStr: ...
@overload
def concat2(x: str, y: str) -> str: ...
@overload
def concat2(x: bytes, y: bytes) -> bytes: ...
某些函數(如上述 map
或 bytes.__ getitem__
)無法用類型變量來做精確表達。但與 @overload
不同,類型變量在存根文件之外也可以使用。建議僅在類型變量不足時才使用 @overload
,因為它只能特定用於存根文件。
類型變量(如 AnyStr
)和采用 @overload
還有另一個重要區別,就是類型變量還可用於定義泛型類的類型參數的限制條件。例如,泛型類 typing.IO
的 type
參數就受到限制(合法類型只有 IO[str]
、IO[bytes]
和 IO[Any]
):
class IO(Generic[AnyStr]): ...
存根文件的存儲和發布(Storing and distributing stub files)
存根文件最簡單的存儲和發布形式,就是將其放入 Python 模塊的同一目錄中。這樣程序員和軟件工具就都能輕松找到他們了。但由於軟件包(package)的維護人員完全可以不在包中加入類型提示,因此通過 Pip 從 PyPI 安裝第三方存根文件也是被支持的。這時必須考慮3個問題:命名、版本管理、安裝路徑。
本 PEP 不提供第三方存根文件包的命名方案建議。但願軟件包的可發現性(discoverability)將會因其普及程度而定,例如 Django 軟件包。
第三方存根文件必須用源代碼包兼容的最低版本進行版本管理。例如:FooPackage
的版本有 1.0、1.1、1.2、1.3、2.0、2.1、2.2。在 1.1、2.0 和 2.2 版本,API 都做過改動。存根文件包的維護人員可以為所有版本任意發布存根文件,但至少需要為 1.0、1.1、2.0 和 2.2 版本發布,以便能讓最終用戶對所有版本進行類型檢查。因為用戶知道最近的較低或相同版本的存根文件是兼容的。在已給出的示例中,對於 FooPackage 1.3 而言,用戶應該選擇1.1版的存根文件。
請注意,如果用戶決定采用可用的“最新”源代碼包,則只要經常更新存根文件,采用最新的存根文件通常也就應該可以了。
第三方存根軟件包可以把存根文件保存在任何位置。類型檢查程序應該用 PYTHONPATH
進行搜索。一定會作檢查的默認后備目錄為 shared/typehints/pythonX.Y/
(某些 PythonX.Y 由類型檢查程序確定,而不僅是已安裝的版本)。因為每種環境只能為給定的 Python 版本安裝一個包,所以在該目錄下不會再區分版本了(就像 pip 在 site-packages 下的純目錄安裝一樣)。存根文件包的作者可以在 setup.py 中采用以下代碼段:
...
data_files=[
(
'shared/typehints/python{}.{}'.format(*sys.version_info[:2]),
pathlib.Path(SRC_PATH).glob('**/*.pyi'),
),
],
...
更新:自2018年6月起,為第三方軟件包發布類型提示存根文件的推薦方式已作更改,除了 typeshed(參閱下一節)之外,現在有了一個用於發布類型提示的標准 PEP 561。它支持可單獨安裝的包,包中可包含存根文件、作為可執行代碼包同步發布的存根文件、行內(inline)類型提示,后兩種方式可通過在包中包含一個名為py.typed
的文件進行啟用。
typeshed 庫(The Typeshed Repo)
有一個共享庫將有用的存根文件都搜集在了一起,那就是 typeshed。存根文件的收集策略是獨立的,並在庫文檔中做了報告。注意,如果某個包的所有者明確要求忽略,則此包的存根文件將不會包含進來。
異常(Exceptions)
針對本特性可引發的異常,沒有語法上的建議。目前本特性唯一已知的應用場景就是作為文檔記錄,這時建議將信息放入文檔字符串(docstring)中。
typing 模塊(The typing Module)
為了將靜態類型檢查特性開放給 Python 3.5 以下的版本使用,需要有一個統一的命名空間。為此,標准庫中引入了一個名為 typing
的新模塊。
typing
模塊定義了用於構建類型的基礎構件(如 Any
)、表示內置集合類的泛型變體類型(如 List
)、表示集合類的泛型抽象基類類型(如 Sequence
)和一批便捷類定義。
請注意,只有在類型注解的上下文中才支持用 TypeVar
定義的特殊類型結構,比如 Any
、Union
和類型變量,而 Generic
則只能用作基類。如果出現在 isinstance
或 issubclass
中,所有這些(未參數化的泛型除外)都將引發 TypeError
異常。
基礎構件:
- Any,用法為
def get(key: str) -> Any: ...
。 - Union,用法為
Union[Type1, Type2, Type3]
。 - Callable,用法為
Callable[[Arg1Type, Arg2Type], ReturnType]
。 - Tuple,用於列出元素類型,比如
Tuple[int, int, str]
。空元組類型可以表示為Tuple[()]
。可變長同構元組可以表示為一個類型和省略號,比如Tuple[int, ...]
,此處的...
是語法的組成部分。 - TypeVar,用法為
X = TypeVar('X', Type1, Type2, Type3)
或簡化為Y = TypeVar('Y')
(詳見上文)。 - Generic,用於創建用戶自定義泛型類。
- Type,用於對類對象做類型注解。
內置集合類的泛型變體:
- Dict,用法為
Dict[key_type, value_type]
。 - DefaultDict,用法為
DefaultDict[key_type, value_type]
,是collections.defaultdict
的泛型變體。 - List,用法為
List[element_type]
。 - Set,用法為
Set[element_type]
。參閱下文有關AbstractSet
的備注信息。 - FrozenSet,用法為
FrozenSet[element_type]
。
注意:Dict
、DefaultDict
、List
、Set
和 FrozenSet
主要用於對返回值做類型注解。而函數參數的注解,建議采用下述抽象集合類型,比如 Mapping
、Sequence
或 AbstractSet
。
容器類抽象基類的泛型變體(及一些非容器類):
- Awaitable
- AsyncIterable
- AsyncIterator
- ByteString
- Callable(詳見上文)
- Collection
- Container
- ContextManager
- Coroutine
- Generator,用法為
Generator[yield_type, send_type, return_type]
,表示生成器函數的返回值。此為Iterable
的子類型。並且為send()
方法可接受的類型加入了類型變量(可逆變)。可逆變的意思是,在要求可發送Manager
實例的上下文中,生成器允許發送Employee
實例,並返回生成器的類型。 - Hashable(非泛型)
- ItemsView
- Iterable
- Iterator
- KeysView
- Mapping
- MappingView
- MutableMapping
- MutableSequence
- MutableSet
- Sequence
- Set,重命名為
AbstractSet
。因為typing
模塊中的Set
表示泛型set()
,所以需要改名。 - Sized(非泛型)
- ValuesView
一些用於測試某個方法的一次性類型,類似於 Hashable
或 Sized
:
- Reversible,用於測試
__reversed__
- SupportsAbs,用於測試
__abs__
- SupportsComplex,用於測試
__complex__
- SupportsFloat,用於測試
__float__
- SupportsInt,用於測試
__int__
- SupportsRound,用於測試
__round__
- SupportsBytes,用於測試
__bytes__
便捷類定義:
- Optional,定義為
Optional[t] == Union[t, None]
。 - Text,只是 Python 3 中
str
、Python 2 中unicode
的別名。 - AnyStr,定義為
TypeVar('AnyStr', Text, bytes)
。 - NamedTuple,用法為
NamedTuple(type_name, [(field_name, field_type), ...])
等價於collections.namedtuple(type_name, [field_name, ...])
。在為命名元組類型的字段進行類型聲明時,這會很有用。 - NewType,用於創建運行開銷很小的唯一類型,如
UserId = NewType('UserId', int)
。 - cast(),如前所述。
- @no_type_check,用於禁止對某個類或函數做類型檢查的裝飾器(參見下文)。
- @no_type_check_decorator,用於創建自定義裝飾器的裝飾器,含義與
@no_type_check
相同(參見下文)。 - @type_check_only,僅在對存根文件做類型檢查時可用的裝飾器,標記某個類或函數在運行時不可用。
- @overload,如上所述。
- get_type_hints(),用於獲取函數或方法的類型提示信息的工具函數。給定一個函數或方法對象,它將以
__annotations__
的格式返回一個dict
,向前引用將在原函數或方法定義的上下文中進行表達式求值。 - TYPE_CHECKING,運行時為
False
,而對類型檢查器則為True
。
I/O相關的類型:
- IO(基於
AnyStr
的泛型) - BinaryIO(只是
IO[bytes]
子類型) - TextIO(只是
IO[str]
子類型)
與正則表達式和 re
模塊相關的類型:
Match
和Pattern
,re.match()
和re.compile()
的結果類型(基於AnyStr
的泛型)。
Python 2.7 和跨版本代碼的建議語法(Suggested syntax for Python 2.7 and straddling code)
某些工具軟件可能想在必須與 Python 2.7 兼容的代碼中支持類型注解。為此,本 PEP 在此給出建議性(而並非強制)擴展,其中函數的類型注解放入 # type
注釋(comment)中。這種注釋必須緊挨着函數頭之后,但在文檔字符串之前。舉個例子,下述 Python 3 代碼:
def embezzle(self, account: str, funds: int = 1000000, *fake_receipts: str) -> None:
"""Embezzle funds from account using fake receipts."""
<code goes here>
等價於以下代碼:
def embezzle(self, account, funds=1000000, *fake_receipts):
# type: (str, int, *str) -> None
"""Embezzle funds from account using fake receipts."""
<code goes here>
請注意,方法的 self
不需要注明類型。
無參數方法則應如下所示:
def load_cache(self):
# type: () -> bool
<code>
有時需要僅為函數或方法指定返回類型,而暫不指定參數類型。為了明確這種需求,可以用省略號替換參數列表。例如:
def send_email(address, sender, cc, bcc, subject, body):
# type: (...) -> bool
"""Send an email message. Return True if successful."""
<code>
參數列表有時會比較長,難以用一條 # type:
注釋來指定類型。為此可以每行給出一個參數,並在每個參數的逗號之后加上必要的 # type:
注釋。返回類型可以用省略號語法指定。指定返回類型不是強制性要求,也不是每個參數都需要指定類型。帶有 # type:
注釋的行應該只包含一個參數。最后一個參數的類型注釋應該在右括號之前。例如:
def send_email(address, # type: Union[str, List[str]]
sender, # type: str
cc, # type: Optional[List[str]]
bcc, # type: Optional[List[str]]
subject='',
body=None # type: List[str]
):
# type: (...) -> bool
"""Send an email message. Return True if successful."""
<code>
注意事項:
- 只要工具軟件支持這種類型注釋語法,就應該與 Python 版本無關。為了支持橫跨 Python 2 和 Python 3 的代碼,必須如此。
- 參數或返回值不得同時帶有類型注解(annotation)和類型注釋(comment)。
- 如果要采用簡寫格式(如
# type: (str, int) -> None
),則每一個參數都必須如此,實例和類方法的第一個參數除外。這第一個參數通常會省略注釋,但也允許帶上。 - 簡寫格式必須帶有返回類型。如果是 Python 3 則會省略某些參數或返回類型,而 Python 2 則應使用
Any
。 - 采用簡寫格式時,
*args
和**kwds
的類型注解前面請對應放置1或2個星號。在用 Python 3 注解格式時,此處的注解表示的是每一個參數值的類型,而不是由特殊參數值args
或kwds
接收到的tuple
/dict
的類型。 - 與其他的類型注釋相類似,類型注解中用到的任何名稱都必須由包含注解的模塊導入或定義。
- 采用簡寫格式時,整個注解必須在一行之內。
- 簡寫格式也可以與右括號處於同一行,例如:
def add(a, b): # type: (int, int) -> int
return a + b
- 類型檢查程序會將位置不對的類型注釋標記為錯誤。如有必要,可以對此類注釋作兩次注釋標記。例如:
def f():
'''Docstring'''
# type: () -> None # Error!
def g():
'''Docstring'''
# # type: () -> None # This is OK
在對 Python 2.7 代碼做類型檢查時,類型檢查程序應將 int
和 long
視為相同類型。對於注解為 Text
的參數,str
和 unicode
類型也應該是可接受的。
未被接受的替代方案(Rejected Alternatives)
在討論本 PEP 的早期草案時,出現過各種反對意見,並提出過一些替代方案。在此討論其中一些意見,並解釋一下未被接受的原因。
下面是幾個主要的反對意見。
泛型參數該用什么括號?(Which brackets for generic type parameters?)
大多數人都熟知,在C++、Java、C# 和 Swift 等語言當中,用尖括號(如 List<int>
)來表示泛型的參數化。這種格式的問題是真的難以解析,尤其是對於像 Python 這種思維簡單的解析器而言。在大多數語言中,通常只允許在特定的語法位置用尖括號來解決歧義,而這些位置不允許出現泛型表達式。並且還得采用非常強大的解析技術,可對任何一段代碼進行重復解析(backtrack)。
但在 Python 中,更願意讓類型表達式(在語法上)與其他表達式一樣,以便用變量賦值之類的操作就能創建類型別名。不妨看看下面這個簡單的類型表達式:
List<int>
從 Python 解析程序的角度來看,以4個短語(名稱、小於、名稱、大於)開頭的表達式將視為連續(chained)比較:
a < b > c # I.e., (a < b) and (b > c)
甚至可以創建一個兩種解析方式共存的示例:
a < b > [ c ]
假設語言中包含尖括號,則以下兩種解釋都是可以的:
(a<b>)[c] # I.e., (a<b>).__getitem__(c)
a < b > ([c]) # I.e., (a < b) and (b > [c])
當然能夠再提出一種規則來消除上述情況的歧義,但對於大多數用戶而言,會覺得這些規則稍顯隨意和復雜。並且這還要將 CPython 解析程序(和其他所有Python 解析程序)做出很大的改動。有一點應該注意,Python 當前的解析程序是有意如此“愚蠢”的,這樣用戶很容易就能想到簡單的語法。
因為上述所有原因,所以方括號(如 List[int]
)是(長期以來都是)泛型參數的首選語法。這通過在元類上定義 __getitem__()
方法就可以實現,根本不需要引入新的語法。這種方案在所有較新版本的 Python(從 Python 2.2 開始)中均有效。並非只有 Python 才選擇這種語法,Scala 中的泛型類也采用了方括號。
已在用的注解怎么辦(What about existing uses of annotations?)
有一條觀點指出,PEP 3107 明確支持在函數注解中使用任意表達式。因此,本條新提案被認為與 PEP 3107 規范不兼容。
對此的回應是,首先本提案沒有引入任何直接的不兼容性,因此使用注解的程序在 Python 3.4 中仍然可以正確運行,在 Python 3.5 中也毫無影響。
類型提示確實期望能最終成為注解的唯一用途,但這需要再多些討論,而且 Python 3.5 才首次推出 typing
模塊,也需要一段時間實現廢棄(deprecation)。直至 Python 3.6 發布之前,當前的 PEP 都將為臨時(provisional)狀態(參閱 PEP 411)。可能的方案最快將自 3.6 開始無提示地廢棄非類型提示的注解,自 3.7 開始完全廢棄,並在 Python 3.8 中將類型提示聲明為唯一允許使用的注解。即便類型提示在一夜之間取得了成功,也應該讓帶注解程序包的作者有足夠的時間去更換方案。
更新:2017年秋季,本 PEP 和 type.py
模塊的臨時狀態終止計划已作更改,因此其他注解用法的棄用計划也已更改。更新過的時間計划請參閱 PEP 563。
另一種可能的結果是,類型提示最終將成為注解的默認含義,但將其禁用的選項也會一直保留。為此,本提案定義了一個裝飾器 @no_type_check
,該裝飾器將禁止對給定類或函數中用作類型提示的注解作默認解釋。這里還定義了一個元裝飾器 @no_type_check_decorator
,可用於對裝飾器進行裝飾,使得用其裝飾的任何函數或類中的注解都會被類型檢查程序忽略。
且還有 # type: ignore
注解呢,靜態檢查程序應支持對選中包禁止類型檢查的配置項。
盡管有這么多選擇可用,但允許類型提示和其他形式的注解共存於參數中的提案已經發布過了一些。有一項提案建議,如果某個參數的注解是字典字面量,則每個字典鍵都表示一種不同格式的注解,字典鍵“type
”將用於類型提示。這種想法及其變體的問題在於,注解會變得非常“雜亂”,可讀性會很差。而且大多數現有采用注解的庫,幾乎不需要與類型提示混合使用。因此,只要有選擇地禁用類型提示就足夠了。
前向聲明的問題(The problem of forward declarations)
當類型提示必須包含向前引用時,當前提案無疑是次優選擇。Python 要求所有變量在用到時再作定義。除了循環導入外,這不太會有問題:這里的“用到”表示“在運行時去作查找”,並且對於大多數“向前”引用而言,確保在用到名稱的函數被調用之前定義名稱就沒有問題了。
類型提示的問題在於,在定義函數時會對注解進行求值(據 PEP 3107,類似於默認值),因此注解中使用的任何名稱在定義函數時必須已經定義。常見的場景是類的定義,其方法需要在注解中引用類本身。更一般地說,在相互遞歸引用的類中也可能發生這種情況。對於容器類型而言這很自然,例如:
class Node:
"""Binary tree node."""
def __init__(self, left: Node, right: Node):
self.left = left
self.right = right
上述寫法是行不通的,因為 Python 的特性就是,一旦類的全體代碼執行完畢,類的名稱就定義完成了。我們的解決方案不是特別優雅,但是可以完成任務,也就是允許在注解中使用字符串字面量。不過大多數時候都不必用到字符串,類型提示的大多數應用都應引用內置類型或其他模塊中已定義的類型。
有一種答復將會修改類型提示的語義,以便根本不會在運行時對其進行求值。畢竟類型檢查是脫機進行的,為什么要在運行時對類型提示進行求值呢。當然這與向下兼容有沖突,因為 Python 解釋程序其實並不知道某個注解是要用作類型提示或是有其他用途。
有一種可行的折衷方案,就是用 __future__
導入可以將給定模塊中的所有注解都轉換為字符串字面量,如下所示:
from __future__ import annotations
class ImSet:
def add(self, a: ImSet) -> List[ImSet]: ...
assert ImSet.add.__annotations__ == {'a': 'ImSet', 'return': 'List[ImSet]'}
這種 __future__
導入語句可能會在單獨的 PEP 中給出。
更新:PEP 563 中已討論了 __future__
導入語句及其后果。
雙冒號(The double colon)
一些有創造力的人已經嘗試發明了多種解決方案。比如有人提議讓類型提示采用雙冒號(:😃,可以一次解決兩個問題:消除類型提示與其他注解之間的歧義、修改語義避免運行時求值。但這種想法有以下幾個問題。
- 難看(ugly)。在 Python 中單個冒號有很多用途,並且看起來都很熟悉,因為類似於英文中的冒號用法。這是一條普遍的經驗法則,Python 會遵守標點符號的大多數使用格式,那些例外通常也是因其他編程語言而熟知的。但是 :: 的這種用法在英語中是聞所未聞的,而在其他語言(例如 C++)中是被用作作用域操作符的,這太與眾不同了。相反,類型提示采用單個冒號讀起來很自然,這不足為奇,因為這是為此目的而精心設計的(想法比 PEP 3107 gvr-artima 要早得多)。從 Pascal 到 Swift,其他很多語言也采用了相同的風格。
- 該如何處理返回類型的注解?
- 實際上這是在運行時對類型提示進行求值的特性。
- 讓類型提示可用於運行時,使得能基於類型提示構建運行時的類型檢查程序。
- 即便代碼尚未運行,類型檢查程序仍能捕獲錯誤。因為類型檢查程序是一個單獨的程序,所以用戶可以選擇不運行(甚至不安裝),但仍可能想把類型提示用作簡明的文檔。錯誤的類型提示即便當作文檔也沒啥用處。
- 因為是新語法,所以把雙冒號用於類型提示將會受到限制,只能適用於 Python 3.5 的代碼。而利用現有的語法,本提案可以輕松應用於較低版本的Python 3。mypy 實際支持 Python 3.2 以上的版本。
- 如果類型提示獲得成功,可能會決定在未來加入新的語法,用於聲明變量的類型,比如
var age: int = 42
。如果參數的類型提示采用雙冒號,那么為了保持一致,未來的語法必須采用同樣的約定,如此難看的語法將會一直流傳下去。
其他一些新語法格式(Other forms of new syntax)
還有一些其他格式的語法也被提出來過,比如引入 where
關鍵字,以及 Cobra-inspired requires
子句。但這些語法都和雙冒號一樣存在同樣的問題,他們不適用於低版本的 Python 3。新的__future__ 導入語法也同樣如此。
其他的向下兼容約定(Other backwards compatible conventions)
提出的想法有:
- 裝飾器,比如
@typehints(name=str, returns=str)
。這可能會有用,但太啰嗦了(增加一行代碼,並且參數名稱必須重復一遍),且與 PEP 3107 的注解方式相去甚遠。 - 存根文件。存根文件確實有需要,但主要是用來把類型提示加入已有代碼中,這些代碼本身不適合添加類型提示,例如:第三方軟件包、需同時支持 Python 2和 Python 3 的代碼、(特別是)擴展模塊。在大多數情況下,與函數定義放在一起的行內注解會更加有用。
- 文檔字符串。文檔字符串對注解已有約定,即基於 Sphinx 注解方式
(:type arg1: description)
。這真有點啰嗦(每個參數增加一行代碼),而且不太優雅。當然再創造一些新語法也是可以的,但很難超越注解語法(因為它是專為此目的而設計的)。
還有人提議,就坐等新的發行版本吧。但這能解決什么問題?只會是拖延下去罷了。
PEP 開發過程(PEP Development Process)
本 PEP 的最新文稿位於 GitHub 上。另有一個議題跟蹤包含了很多技術討論內容。
GitHub 上的文稿會定期進行小幅更新。通常正式的 PEPS 庫只在新文稿發布到 python-dev 時才會更新。
致謝(Acknowledgements)
沒有 Jim Baker、Jeremy Siek、Michael Matson Vitousek、Andrey Vlasovskikh、Radomir Dopieralski、Peter Ludemann 和BDFL-Delegate、Mark Shannon 的寶貴投入、鼓勵和建議,本文就無法完成。
本文受到 PEP 482 中提及的現有語言、庫和框架的影響。非常感謝其創建者(按字母序排列):Stefan Behnel、William Edwards、Greg Ewing、Larry Hastings、Anders Hejlsberg、Alok Menghrajani、Travis E. Oliphant、Joe Pamer、Raoul-Gabriel Urma、and Julien Verlaguet。
參考文獻(References)
[mypy] http://mypy-lang.org
[gvr-artima] http://www.artima.com/weblogs/viewpost.jsp?thread=85551
[wiki-variance] http://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29
[typeshed] https://github.com/python/typeshed/
[pyflakes] https://github.com/pyflakes/pyflakes/
[pylint] http://www.pylint.org
[roberge] http://aroberge.blogspot.com/2015/01/type-hinting-in-python-focus-on.html