[译]PEP 484 -- 类型提示

PEP 原文:https://www.python.org/dev/peps/pep-0484/

创建日期:2014-09-29

合入版本:3.5

译者:meng hu

译注:PEP 484 快速翻译。省略了很多的内容,只是为了快速入门。

Abstract

此方案还是 provisional 的,此方案并没有阻止其他的 annotation,或者强制或者禁止使用一种特定的 annotation 的过程。

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

会在运行时添加一个__annotations__的属性,但是不会再运行时进行类型检查。本方案假设有额外的单独的 off-line 的 type checker 工具进行类型检查,当然这个检查的过程也是用户自己选择的。

本提案更多的收到 mypy 这个工具的灵感。比如说”sequence of integers”会被写作Sequence[int]。这个Swquencetyping模块原生提供。

type system 支持 unions,generic 类型, Any类型兼容所有的类型。his latter feature is taken from the idea of gradual typing. Gradual typing and the full type system are explained in PEP 483.

The meaning of annotations

所有没有被 annotation 的函数都应该被认为是拥有最 general 的类型,应该被 type checker 进行忽略。如果一个函数被@no_type_check装饰器装饰,都应该被认为不带任何的 annotations

类的实例函数或者类函数的第一个参数是此类型,这很好理解。并且__init__函数的函数值如果有 type hint 的话,应该是->None这主要是为了明确这是有 type hint 的,否则的话def __int__(self)到底是有类型 hint 还是没有?

Type Definition Syntax

简单来说

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

Acceptable type hints

可接受的 type hint 包括内置的类(包括标准库中的类,或者第三方模块中的类),abstract base classes, 或者types模块中的类型,或者用户自定义的类型(包括标准库中定义的类型,或者第三方模块中的类型)

annotation 仅仅是一些特殊的注释,或者一个 stub file。除此之外并无特殊之处

annotation 应该足够的简单,否则的话静态分析工具并不能很好的解析类型。比如说动态计算的类型是不会被允许的。

In addition to the above, the following special constructs defined below may be used: None, Any, Union, Tuple, Callable, all ABCs and stand-ins for concrete classes exported from typing (e.g. Sequence and Dict), type variables, and type aliases.

Using None

在使用 type hint 时,None相当于type(None)

Type aliases

简单来说,就是这样:

Url = str

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

Url尽量首字母大写,这样体现了这是用户自定义的类型。

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]

在 type hint 中可以接受的东西,在 type alias 中都是可以接受的,比如说,上面按个等价于下面这个

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] 前面的是接受参数的类型,后面那个是返回的类型。

当然,也可以Callable[..., str]的省略号,表示不在乎接受的参数的类型,这里的省略号没有方括号,这点需要额外注意。

typing.callable将重复的进行 collections.abc.Callable 的工作,isinstance(x, typing.Callable)会被解析成isinstance(x, collections.abc.Callable),但是isinstance(x, typing.Callable[...])不会被支持。

泛型 Generics

对于容器类来说,容器类中的对象类型是不能静态引用。所以typing中的 abstract base class 或许比较有用

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]

这个函数必须被直接赋值给一个变量,而不能作为一个大的表达式的中间结果。此外,这个函数的参数必须是个字符串,且应该是被赋值给的那个变量的 name。并且,同一个 type variable 不能被重定义。

除此之外,也可以创建一个只包含指定类型的泛型,在下面这个例子中,AnyStr只能是str或者bytes

from typing import TypeVar

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

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

对于下面这个例子中,两个参数不会被认为是MyStrl类型,而是被认为是str类型,并且返回值 x 的类型也是 str 而不是 MyStr

class MyStr(str): ...

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

Any被认为是任意类型,比如下面例子中的泛型被认为是elements: List

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

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))

有意思的是,这个[]真的是会实现__getitem__方法,以返回基于 T 这个类型的 LoggedVar 类,比如下面是合法的

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]):
...

但是这个是错误的:

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]):
...

Scoping rules for type variables

十分符合直觉,不看也罢,大概就是T是有范围的,对于一个类的实例,一点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, 展示泛型类以及类型擦除

类型可以推断出来,但是推断不出来就出错(废话),但是在运行时,不同具体泛型类的对象是同一个类型的,也就是说类型擦除了,运行时不记录泛型类型。就像 typescript 或者 java 那样。

Using generic classes (parameterized or not) to access attributes will result in type check failure. Outside the class definition body, a class attribute cannot be assigned, and can only be looked up by accessing it through a class instance that does not have an instance attribute with the same name:

对于虚 collections 比如Mapping或者Sequence以及内置类型的泛型类List, Dict, Set, FrozenSet不能够被实例化,但是一个 concrete(实在的) 自定义子类或者 generic 版本的 concrete 容器类可以被实例化

data = DefaultDict[int, bytes]()

最好不要用Node[int]这样的东西在表达式中,应该用 aliasIntNode=Node[Int],因为前者会带来运行时的消耗

Arbitrary generic types as base classes

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 is a subclass of dict and a subtype of Dict[str, List[Node]].

from typing import TypeVar, Iterable, Container

T = TypeVar('T')

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

然后引用LinkedList[int]合法

Abstract generic types

Type variables with an upper bound

指定这个类型是某个具体边缘类型的子类型

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

Covariance and contravariance

简单点来说就是如果EmployeeManager的父类,List[Employee]List[Manager]的父类(convariance),或者说是子类(contravariance)吗?至少在目前的 PEP 中,默认都不是。当然可以设置

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
from typing import TypeVar

class Employee: ...

class Manager(Employee): ...

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

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

dump_employee(Manager()) # OK
# while the following is prohibited:
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

标准库中有 numbers 这个模块有一系列的 ABCs

Forward references

合法

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 表达式就行。

Union types

from typing import Union

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

def handle_employee(e: Optional[Employee]) -> None: ...
# Optional[Employee]等价于Union[Employee, None]

事实上,对于以前的 PEP 来说,允许那些标注了类型,但是值是 None 的那些东西,认为是 Optional

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

Support for singleton types in unions

The Any type

它和object完全不是一个东西。

The NoReturn type

表识函数不返回任何值。连 None 都不能返回,表示,此函数必然触发异常或者 exit。如果有某个分支导致可能有返回值,就会出错,比如下面第二个例子

from typing import NoReturn

def stop() -> NoReturn:
raise RuntimeError('no way')
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

并且,这个只能用在函数标注上。

The type of class objects

有些时候,一个函数可能需要传入一个类对象,而不是一个类的实例。这个时候应该用type[C]这个表示,传入的参数可以是C的子类, 而不是 C 的实例。

Annotating instance and class methods

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

对于简单的平台验证,静态工具还是可以以来的

Runtime or type checking

Arbitrary argument lists and default argument values

Positional-only arguments

函数参数中,以__开头的都是 positional-only 参数,除非它的结尾也有__

Annotating generator functions and coroutines

对于生成器:Generator[yield_type, send_type, return_type]

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

对于 await

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

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

想了解更多,还是看原文

Compatibility with other uses of function annotations

下面三种情况不会被类型检查

  • a # type: ignore comment;
  • a @no_type_check decorator on a class or function;
  • a custom class or function decorator marked with @no_type_check_decorator.

Type comments

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 frobnicate() as foo: # type: int
# Here foo is an int
...

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

在标注文件中,可以这么做,尤其适合那些不想给初始值的地方。这些对所有的 python 版本都支持

from typing import IO

stream: IO[str]

在 python3.5,但是没有 stub 的地方可以这么做

from typing import IO

stream = None # type: IO[str]

Casts

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])

NewType helper function

NewType("unique_name", base_class)

减少运行时的开销。

Stub files

用在什么地方:

  • Extension modules
  • Third-party modules whose authors have not yet added type hints
  • Standard library modules for which type hints have not yet been written
  • Modules that must be compatible with Python 2 and 3
  • Modules that use annotations for other purposes

Function/method overloading

存储和分发 stub files

The Typeshed Repo

Exceptions

The typing Module

应该去原文读一读,工具类的东西

Suggested syntax for python2.7 and straddling code

如果需要在 python2.7 上实现 type hint,可能需要去读一读

Rejected Alternatives