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