前言

备注:Python 运行时不强制要求函数与变量类型注解。它们可用于类型检查器、IDE、错误检查器等第三方工具。

本模块提供对类型提示的运行时支持。对于类型系统的原始说明,请参阅 PEP 484。一个更简明的介绍是 PEP 483。

下面的函数接收与返回的都是字符串,注解方式如下:

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

greeting 函数中,参数 name 的类型应是 str,返回类型是 str。子类型也可以作为参数。

新的功能频繁地被添加到 typing 模块中。typing_extensions 包提供了这些新功能对旧版本 Python 的向后移植。

要获取已弃用特性及其弃用时间线的概要,请参阅 Deprecation Timeline of Major Features。

操作系统:Windows 10 专业版

参考文档

  1. typing —— 对类型提示的支持

类型别名

类型别名是通过为类型赋值为指定的别名来定义的。 在本例中,Vectorlist[float] 将被视为可互换的同义词:

1
2
3
4
5
6
7
Vector = list[float]

def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]

# passes type checking; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])

类型别名适用于简化复杂的类型签名。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections.abc import Sequence

ConnectionOptions = dict[str, str]
Address = tuple[str, int]
Server = tuple[Address, ConnectionOptions]

def broadcast_message(message: str, servers: Sequence[Server]) -> None:
...

# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
message: str,
servers: Sequence[tuple[tuple[str, int], dict[str, str]]]) -> None:
...

类型别名可以用 TypeAlias 来标记,以显式指明该语句是类型别名声明,而不是普通的变量赋值:

1
2
3
from typing import TypeAlias

Vector: TypeAlias = list[float]

NewType

使用 NewType 助手来创建不同的类型

1
2
3
4
from typing import NewType

UserId = NewType('UserId', int)
some_id = UserId(524313)

静态类型检查器把新类型当作原始类型的子类,这种方式适用于捕捉逻辑错误:

1
2
3
4
5
6
7
8
def get_user_name(user_id: UserId) -> str:
...

# passes type checking
user_a = get_user_name(UserId(42351))

# fails type checking; an int is not a UserId
user_b = get_user_name(-1)

UserId 类型的变量可执行所有 int 操作,但返回结果都是 int 类型。这种方式允许在预期 int 时传入 UserId,还能防止意外创建无效的 UserId:

1
2
# 'output' is of type 'int', not 'UserId'
output = UserId(23413) + UserId(54341)

注意,这些检查只由静态类型检查器强制执行。在运行时,语句 Derived = NewType(‘Derived’, Base) 将产生一个 Derived 可调用对象,该对象立即返回你传递给它的任何参数。 这意味着语句 Derived(some_value) 不会创建一个新的类,也不会引入超出常规函数调用的很多开销。

更确切地说,在运行时,some_value is Derived(some_value) 表达式总为 True。

创建 Derived 的子类型是无效的:

1
2
3
4
5
6
from typing import NewType

UserId = NewType('UserId', int)

# Fails at runtime and does not pass type checking
class AdminUserId(UserId): pass

然而,我们可以在 “派生的” NewType 的基础上创建一个 NewType。

1
2
3
4
5
from typing import NewType

UserId = NewType('UserId', int)

ProUserId = NewType('ProUserId', UserId)

同时,ProUserId 的类型检查也可以按预期执行。

备注: 请记住使用类型别名将声明两个类型是相互 等价 的。 使用 Alias = Original 将使静态类型检查器在任何情况下都把 Alias 视为与 Original 完全等价。 这在你想要简化复杂的类型签名时会很有用处。

反之,NewType 声明把一种类型当作另一种类型的 子类型。Derived = NewType(‘Derived’, Original) 时,静态类型检查器把 Derived 当作 Original 的 子类 ,即,Original 类型的值不能用在预期 Derived 类型的位置。这种方式适用于以最小运行时成本防止逻辑错误。

标注可调用对象

函数 – 或其他 callable 对象 – 可以使用 collections.abc.Callabletyping.Callable 来标注。 Callable[[int], str] 表示一个接受 int 类型的单个参数并返回 str 的函数。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
from collections.abc import Callable, Awaitable

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

async def on_update(value: str) -> None:
... # Body

callback: Callable[[str], Awaitable[None]] = on_update

下标语法总是要刚好使用两个值:参数列表和返回类型。 参数列表必须是一个由类型组成的列表、ParamSpec、Concatenate 或省略号。 返回类型必须是单一类型。

如果将一个省略号字面值 ... 作为参数列表,则表示可以接受包含任意形参列表的可调用对象:

1
2
3
4
5
6
def concat(x: str, y: str) -> str:
return x + y

x: Callable[..., str]
x = str # OK
x = concat # Also OK

Callable 无法表达复杂的签名如接受可变数量参数的函数、重载的函数 或具有仅限关键字形参的函数。 但是,这些签名可通过定义具有 __call__() 方法的 Protocol 类来表达:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from collections.abc import Iterable
from typing import Protocol

class Combiner(Protocol):
def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: ...

def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes:
for item in data:
...

def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]:
...
def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]:
...

batch_proc([], good_cb) # OK
batch_proc([], bad_cb) # Error! Argument 2 has incompatible type because of
# different name and kind in the callback

以其他可调用对象为参数的可调用对象可以使用 ParamSpec 来表明其参数类型是相互依赖的。 此外,如果该可调用对象增加或删除了其他可调用对象的参数,可以使用 Concatenate 操作符。 它们分别采取 Callable[ParamSpecVariable, ReturnType] 和 Callable[Concatenate[Arg1Type, Arg2Type, …, ParamSpecVariable], ReturnType] 的形式。

在 3.10 版更改: Callable 现在支持 ParamSpec 和 Concatenate。 详情见 PEP 612。

参见: ParamSpec 和 Concatenate 的文档提供了在 Callable 中使用的例子。

泛型(Generic)

由于无法以通用方式静态地推断容器中保存的对象的类型信息,标准库中的许多容器类都支持下标操作来以表示容器元素的预期类型。

1
2
3
4
5
6
7
8
9
10
from collections.abc import Mapping, Sequence

class Employee: ...

# Sequence[Employee] indicates that all elements in the sequence
# must be instances of "Employee".
# Mapping[str, str] indicates that all keys and all values in the mapping
# must be strings.
def notify_by_email(employees: Sequence[Employee],
overrides: Mapping[str, str]) -> None: ...

Generics can be parameterized by using a factory available in typing called TypeVar.

1
2
3
4
5
6
7
from collections.abc import Sequence
from typing import TypeVar

T = TypeVar('T') # Declare type variable "T"

def first(l: Sequence[T]) -> T: # Function is generic over the TypeVar "T"
return l[0]

Annotating tuples

对于 Python 中的大多数容器,类型系统假定容器中的所有元素都是相同类型的。例如:

1
2
3
4
5
6
7
8
9
10
11
from collections.abc import Mapping

# Type checker will infer that all elements in ``x`` are meant to be ints
x: list[int] = []

# Type checker error: ``list`` only accepts a single type argument:
y: list[int, str] = [1, 'foo']

# Type checker will infer that all keys in ``z`` are meant to be strings,
# and that all values in ``z`` are meant to be either strings or ints
z: Mapping[str, str | int] = {}

list 只接受一个类型参数,因此类型检查程序会对上述对 y 的赋值报错。同样, Mapping 只接受两个类型参数:第一个参数表示键的类型,第二个参数表示值的类型。

然而,与大多数其它 Python 容器不同的是,在 Python 惯用代码中,元组中的元素并不都是相同类型的,这种情况很常见。因此,在 Python 的类型系统中,元组被特殊化了。tuple 接受任意数量的类型参数:

1
2
3
4
5
6
7
8
9
10
# OK: ``x`` is assigned to a tuple of length 1 where the sole element is an int
x: tuple[int] = (5,)

# OK: ``y`` is assigned to a tuple of length 2;
# element 1 is an int, element 2 is a str
y: tuple[int, str] = (5, "foo")

# Error: the type annotation indicates a tuple of length 1,
# but ``z`` has been assigned to a tuple of length 3
z: tuple[int] = (1, 2, 3)

要表示一个 任意 长度的元组,并使其中所有元素的类型都为T ,请使用tuple[T, ...] 。要表示空元组,请使用tuple[()] 。使用tuple 作为注释等同于使用tuple[Any, ...]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x: tuple[int, ...] = (1, 2)
# These reassignments are OK: ``tuple[int, ...]`` indicates x can be of any length
x = (1, 2, 3)
x = ()
# This reassignment is an error: all elements in ``x`` must be ints
x = ("foo", "bar")

# ``y`` can only ever be assigned to an empty tuple
y: tuple[()] = ()

z: tuple = ("foo", "bar")
# These reassignments are OK: plain ``tuple`` is equivalent to ``tuple[Any, ...]``
z = (1, 2, 3)
z = ()

类对象 的类型

A variable annotated with C may accept a value of type C. In contrast, a variable annotated with type[C] (or typing.Type[C]) may accept values that are classes themselves – specifically, it will accept the class object of C. For example:

1
2
3
a = 3         # Has type ``int``
b = int # Has type ``type[int]``
c = type(a) # Also has type ``type[int]``

Note that type[C] is covariant:

1
2
3
4
5
6
7
8
9
10
11
12
13
class User: ...
class ProUser(User): ...
class TeamUser(User): ...

def make_new_user(user_class: type[User]) -> User:
# ...
return user_class()

make_new_user(User) # OK
make_new_user(ProUser) # Also OK: ``type[ProUser]`` is a subtype of ``type[User]``
make_new_user(TeamUser) # Still fine
make_new_user(User()) # Error: expected ``type[User]`` but got ``User``
make_new_user(int) # Error: ``type[int]`` is not a subtype of ``type[User]``

The only legal parameters for type are classes, Any, type variables, and unions of any of these types. For example:

1
2
3
4
5
6
7
def new_non_team_user(user_class: type[BasicUser | ProUser]): ...

new_non_team_user(BasicUser) # OK
new_non_team_user(ProUser) # OK
new_non_team_user(TeamUser) # Error: ``type[TeamUser]`` is not a subtype
# of ``type[BasicUser | ProUser]``
new_non_team_user(User) # Also an error

type[Any] is equivalent to type, which is the root of Python’s metaclass hierarchy.

用户定义的泛型类型

用户定义的类可以定义为泛型类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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('%s: %s', self.name, message)

Generic[T] as a base class defines that the class LoggedVar takes a single type parameter T . This also makes T valid as a type within the class body.

The Generic base class defines __class_getitem__() so that LoggedVar[T] is valid as a type:

1
2
3
4
5
from collections.abc import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)

一个泛型可以有任何数量的类型变量。所有种类的 TypeVar 都可以作为泛型的参数:

1
2
3
4
5
6
7
8
from typing import TypeVar, Generic, Sequence

T = TypeVar('T', contravariant=True)
B = TypeVar('B', bound=Sequence[bytes], covariant=True)
S = TypeVar('S', int, str)

class WeirdTrio(Generic[T, B, S]):
...

Generic 类型变量的参数应各不相同。下列代码就是无效的:

1
2
3
4
5
6
7
from typing import TypeVar, Generic
...

T = TypeVar('T')

class Pair(Generic[T, T]): # INVALID
...

您可以通过 Generic 来使用多重继承:

1
2
3
4
5
6
7
from collections.abc import Sized
from typing import TypeVar, Generic

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
...

When inheriting from generic classes, some type parameters could be fixed:

1
2
3
4
5
6
7
from collections.abc import Mapping
from typing import TypeVar

T = TypeVar('T')

class MyDict(Mapping[str, T]):
...

比如,本例中 MyDict 调用的单参数,T。

Using a generic class without specifying type parameters assumes Any for each position. In the following example, MyIterable is not generic but implicitly inherits from Iterable[Any]:

1
2
3
4
from collections.abc import Iterable

class MyIterable(Iterable): # Same as Iterable[Any]
...

用户定义的通用类型别名也同样被支持。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from collections.abc import Iterable
from typing import TypeVar
S = TypeVar('S')
Response = Iterable[S] | int

# Return type here is same as Iterable[str] | int
def response(query: str) -> Response[str]:
...

T = TypeVar('T', int, float, complex)
Vec = Iterable[tuple[T, T]]

def inproduct(v: Vec[T]) -> T: # Same as Iterable[tuple[T, T]]
return sum(x*y for x, y in v)

User-defined generics for parameter expressions are also supported via parameter specification variables in the form Generic[P]. The behavior is consistent with type variables’ described above as parameter specification variables are treated by the typing module as a specialized type variable. The one exception to this is that a list of types can be used to substitute a ParamSpec:

1
2
3
4
5
6
7
8
9
>>> from typing import Generic, ParamSpec, TypeVar

>>> T = TypeVar('T')
>>> P = ParamSpec('P')

>>> class Z(Generic[T, P]): ...
...
>>> Z[int, [dict, float]]
__main__.Z[int, (<class 'dict'>, <class 'float'>)]

Furthermore, a generic with only one parameter specification variable will accept parameter lists in the forms X[[Type1, Type2, ...]] and also X[Type1, Type2, ...] for aesthetic reasons. Internally, the latter is converted to the former, so the following are equivalent:

1
2
3
4
5
6
>>> class X(Generic[P]): ...
...
>>> X[int, str]
__main__.X[(<class 'int'>, <class 'str'>)]
>>> X[[int, str]]
__main__.X[(<class 'int'>, <class 'str'>)]

Note that generics with ParamSpec may not have correct __parameters__ after substitution in some cases because they are intended primarily for static type checking.

用户定义的泛型类可以将 ABC 作为基类而不会导致元类冲突。 参数化泛型的输出结果会被缓存,且 typing 模块中的大多数类型都是 hashable 并且支持相等性比较。

Any 类型

Any 是一种特殊的类型。静态类型检查器认为所有类型均与 Any 兼容,同样,Any 也与所有类型兼容。

也就是说,可对 Any 类型的值执行任何操作或方法调用,并赋值给任意变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from typing import Any

a: Any = None
a = [] # OK
a = 2 # OK

s: str = ''
s = a # OK

def foo(item: Any) -> int:
# Passes type checking; 'item' could be any type,
# and that type might have a 'bar' method
item.bar()
...

注意,Any 类型的值赋给更精确的类型时,不执行类型检查。例如,把 a 赋给 s,在运行时,即便 s 已声明为 str 类型,但接收 int 值时,静态类型检查器也不会报错。

此外,未指定返回值与参数类型的函数,都隐式地默认使用 Any:

1
2
3
4
5
6
7
8
9
def legacy_parser(text):
...
return data

# A static type checker will treat the above
# as having the same signature as:
def legacy_parser(text: Any) -> Any:
...
return data

需要混用动态与静态类型代码时,此操作把 Any 当作 应急出口。

Any 和 object 的区别。与 Any 相似,所有类型都是 object 的子类型。然而,与 Any 不同,object 不可逆:object 不是 其它类型的子类型。

就是说,值的类型是 object 时,类型检查器几乎会拒绝所有对它的操作,并且,把它赋给更精确的类型变量(或返回值)属于类型错误。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def hash_a(item: object) -> int:
# Fails type checking; an object does not have a 'magic' method.
item.magic()
...

def hash_b(item: Any) -> int:
# Passes type checking
item.magic()
...

# Passes type checking, since ints and strs are subclasses of object
hash_a(42)
hash_a("foo")

# Passes type checking, since Any is compatible with all types
hash_b(42)
hash_b("foo")

使用 object,说明值能以类型安全的方式转为任何类型。使用 Any,说明值是动态类型。

名义子类型 vs 结构子类型

最初 PEP 484 将 Python 静态类型系统定义为使用 名义子类型。这意味着当且仅当类 A 是 B 的子类时,才满足有类 B 预期时使用类 A 。

此项要求以前也适用于抽象基类,例如,Iterable 。这种方式的问题在于,定义类时必须显式说明,既不 Pythonic,也不是动态类型式 Python 代码的惯用写法。例如,下列代码就遵从了 PEP 484 的规范:

1
2
3
4
5
6
from collections.abc import Sized, Iterable, Iterator

class Bucket(Sized, Iterable[int]):
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...

PEP 544 允许用户在类定义时不显式说明基类,从而解决了这一问题,静态类型检查器隐式认为 Bucket 既是 Sized 的子类型,又是 Iterable[int] 的子类型。这就是 结构子类型 (又称为静态鸭子类型):

1
2
3
4
5
6
7
8
9
from collections.abc import Iterator, Iterable

class Bucket: # Note: no base classes
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...

def collect(items: Iterable[int]) -> int: ...
result = collect(Bucket()) # Passes type check

此外,结构子类型的优势在于,通过继承特殊类 Protocol ,用户可以定义新的自定义协议(见下文中的例子)。

结语

第九十一篇博文写完,开心!!!!

今天,也是充满希望的一天。