首页 > Python基础教程 >
-
PEP 484 类型提示 -- Python官方文档译文 [原创](3)
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