Back to prev

Python Typing Notes

Jul 1, 2023
Linkang Chan
@Jesse Chan

本文记录在 Python 中 Typing 库提供的一些功能,以此来更好的定义日常开发中的类型,当有类型时,我们可以更好的知道某些 API 的输入输出的类型,利人利己。

Type Aliases

new in v3.10

在 Python 中某些类型会比较复杂,我们可以对其做一个类型别名, 起到简化的作用。 同时在创建这样的一个别名的同时,我们可以对这个新的别名设置类型TypeAlias

from typing import TypeAlias

Vector: TypeAlias = list[float]

NewType

顾名思义,这会创建一个新的类型,同时支持从这新的类型创建另外一个新的类型,不过不支持将这个类型作为类的父类。

from typing import NewType

BookId = NewType('BookId', int)
book_id = BookId(1234)

type(book_id) # <class 'int'>

new_book_id = BookId(1) + BookId(2)
# new_book_id type is int, not BookId

当我们定义了新的类型后,如果入参和期望的类型不一致时,会有 warning,但是不影响 runtime。

def query_book_info(book_id: BookId) -> dict:
    ...

book_id = BookId(1234)
_ = query_book_info(book_id)  # static type checker feels good
_ = query_book_info(1234) # static type checker complain about the type int not BookId

另外一些注意


SubBookId = NewType('SubBookId', BookId) # correct

_ = query_book_info(SubBookId(123)) # static type still complain about that

# 不可作为继承使用
class SubBookId(BookId): # invalid !
    pass

Callable

基本结构为Callable[[Arg1Type, Arg2Type], ReturnType]

Generics

Python 中范型的定义比较简单,比如:

from collections.abc import Sequence
from typing import TypeVar

T = TypeVar('T')

def first(l: Sequence[T]) -> T:
    ...

Annotating tuples

正常我们在定义 list 等容器类型时,只能定义一种类型,如果我们的数据里面有不同的类型,这个时候就可以使用 annotating tuples 的类型了

x: list[int] = []

x: tuple[int, str] = (1, 'foo')  # 可以是不同的类型的结果

x: tuple[int, ...]  # 表示 x 可是任意长度的 int 类型

Typing Module Content

AnyStr

这个类型表示可以接受的参数类型是str或者bytes。 但是注意在同时使用多个的时候,需要注意入参要保持一致。比如:

def show_content(ctx1: AnyStr, ctx2: AnyStr) -> None

show_content(b'abc', 'def') # static type checker complain about that, error, cannot mix str and bytes

LiteralString

new in v3.11

所有字符串字面量都是LiteralString, 但是str不是LiteralString

Never

new in v3.11

通常用来定义永远不会被调用或函数不会返回的那种

def never_call_me(arg: Never) -> None:
    ...

never_call_me(_) # ok
never_call_me('abc') # type checker error

在低版本中我们定义无返回可以使用NoReturn

Self

new in v3.11

顾名思义,我们可以在classmethod中使用。

Special forms

Union

表示可以同时可以接受的类型,在 v3.10 中可以使用|代替,有几个规则

Union[str, str ]  ==> str
Union[int, str, [float, dict]] ==> Union[int, str, float, dict]

为了兼容低版本,可以使用 Union,|在自己的脚本中使用即可。

Optional

可以理解成是 Union[X, None], 不过当参数需要显式的负值为 None 的时候,使用 Optional 比较合适。

def foo(arg: Optional[int] = None) -> None:
    ...

Callable

Callable[InputTypes, ReturnTypes]用来定义函数或者方法的,一般结合ConcatenateParamSpec使用。

Concatenate

new in v3.10

Concatenate 当前只能用在 Callable 的第一个参数中,一般我们结合ParamSpec使用。更多的时候是创建一个 decorator 的时候。

比如我们函数接受多个参数,其中某个参数是需要明确给出变量定义的,而其他的参数可以被归类成一组,这个时候我们可以这么定义函数的类型

def my_func(arg1: str, arg2: int, arg3: dict) -> int:
    ...

可以通过如下的方式定义这个函数的类型

P = ParamSpec('P')
R = TypeVar('R')

Callable[Concatenate[str, P], R]

现在我们可以这样定义个 decorator


def my_dec(f: Callable[Concatenate[str, P], R]) -> Callable[P, R]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> R:
        return f('hello', *args, **kwargs)
    return inner


@my_dec
def my_func(arg1: str, arg2: int, arg3:dict) -> int:
    ...

# arg1 is pass in  decorator
my_func(arg2, arg3)

Literial

正常情况下来表示该参数是一组字面量,且是定义中提供的字面量。 一般类型检查器会对其做校验。

mode = Literial['r', 'rb', 'w', 'wb']

ClassVar

顾名思义,用于标记 class variable,可以在dataclasses中使用。

Final

通过 Final 标记的变量在任何时候都不能被赋值(runtime时不影响)。

MAX_SIZE: Final = 9000
MAX_SIZE += 1 # tpye checker report error