【Python】类型注解(3.5+)

一、基本类型

link

使用python的标准库typingPython3.5+)可以为变量、函数、类提供类型注解,但在运行期,解释器并不会对注解的变量进行类型检查。

类型注解也没有静态类型检查的要求,需要mypy/pylint等静态检查器,或者一个足够现代,集成了静态代码分析的IDE

如果这样,我们就可以

1.通过类型推导自动获取调用函数的签名、返回值类型,提高可读性与开发效率

2.通过类型检查,让运行期潜在的类型安全问题提前暴露

单变量注解的语法为:

apple: int = 0 + 0.0
# or
apple: "int" = 2.3

上述代码会在Pycharm2023提示一个warning: Expected type 'int', got 'float' instead

这个语法补丁在一些场合不适用,比如元组解构

butter: int, charlie: float = (114, 5.14)

因为根据官方文档[link]中的文法,python中带注解的赋值表达式的左手侧只能单变量,不支持解构

可以改写成下方丑陋的形式

butter: int
charlie: float
butter, charlie = (114, 5.14)

注意butter: int并非butter的声明,在赋值前使用会抛出undefined NameError

函数注解中,使用带注解的赋值表达式的语法注解形参,函数体使用->与尾置类型来注解返回值类型

def stoi(s: str, base: int = 10) -> int:
    return int(s, base=base)
b: int = stoi("107")

在运行期,通过__annotations__typing.get_type_hints可以获取注解的函数签名与返回类型,对于全局变量,使用globals()['__annotations__']

print(type(stoi))
# <class 'function'>
print(stoi.__annotations__)
# {'s': <class 'str'>, 'base': <class 'int'>, 'return': <class 'int'>}

print(typing.get_type_hints(stoi))

使用管道符|可以从不同类型创建可变类型(python3.10+),如

duff: int | float = 1.2
duff = int(duff)
print(duff)
# 1

二、typing

typing 模块(python3.5+)提供了复合注解功能

1.类型约束

聚合类型

使用typing.Union[...Types]创建可变类型,等效于类型间的管道运算符

var = Union[T1, T2, T3]
assert(var == T1 | T2 | T3)
# a ref type may holding type T1 or T2 or T3
NoReturn

注解函数返回值类型,标识函数不能通过正常控制流返回,所以函数体若只有returnpass,也是类型错误的,因为会隐式地return None

from typing import *
def handler_bad_request(state: my.HttpConnectState) -> NoReturn:
    code: int = my.parse_http_error_code(state)
    raise my.BadHttpRequestError(code)
无返回值函数

尾置类型为Nonetype(None)注解一个无返回值函数(因为隐式返回None),当然None是一个类型为NoneType的单例对象,不是真正的无返回值,不能对位于c/c++中的void。以下代码可通过mypy检查

def get_none() -> None:
    return None
可空类型

使用typing.Optional[T]注解一个可空类型(常用于形参、返回值),其类型为NoneT,即等效于Union[None, T],对位于csharp中的可空值类型(Nullable<T>),c++17中的std::optional<T>。使用Optional[T]作为函数返回值,可以提醒调用者处理空值None

动态类型

typing.Any不约束类型,也就是python本质的动态类型,使用typing.Any时,优先考虑UnionTypeVar泛型注解平替,因为Any无法体现函数输入输出的类型关系。对位于csharp中的动态类型(dynamic),c++17中的std::any

函数对象

typing.Callable在注解中相当于collections.abc.Callable抽象类,其中的纯虚函数为__call__,使用typing.Callable注解def定义的函数或实现__call__的对象,一个举例是装饰器等高阶函数

from typing import *
import time

def tictoc(func: Callable) -> Callable:
    def wrapper(*args, **kwargs):
        tic = time.time()
        func(*args, **kwargs)
        print(f"clock time cost: {time.time() - tic}s")
    return wrapper
@tictoc
def f() -> None:
    time.sleep(1)
f()

如果关心函数签名,则def f(arg1: T1, ... argn: TN) -> RT注解为Callable[[T1, T2, ..., TN], RT]

类型类型

typing.Type在注解中相当于typetype也是一个内置类,且type(type) == type

之所以要像Callable使用一个代理类是因为注解中,有时必要使用[]来表达类型的约束,而python3.9前的type没有[]下标运算符,list, map, set等内置类型也没有

因为type(type) == type,所以可以用Type注解一个运行期决定的类型

import numpy as np
from typing import *
import random
dummy = np.zeros((3, 3), dtype=np.int32)
def up_cast(arr: np.ndarray) -> np.ndarray:
    t: Type = random.choice([np.single, np.half, np.double])
    return arr.astype(t)
print(up_cast(dummy))
Self

python注解中,以下代码会让解释器停止思考

class Foo: 
    def clone(self) -> Foo:
        return self
f = Foo().clone()

在第2行,似乎解释器还未解析完Foo类型,使用Foo注解引起了undefined error,为此要前向声明这个类型,如使用TypeVar封装Foo用于注解。另外使用Self可以达到相同效果,可以将代码改成以下两种相当丑陋的形式

from typing import Self
class Foo: 
    def clone(self: Self) -> Self:
        return self
f = Foo().clone()

or

class Foo: 
    def clone(self: "Foo") -> "Foo":
        return self
f = Foo().clone()
鸭子类型

鸭子类型中,类型匹配不在于继承关系,而是在某种上下文中,一种类型能否在syntax上替换某种未确定的类型

cpp模板便是利用静态鸭子类型实现编译期泛型的,如任何实现了operator*=(const T&)operator++()operator!=(const T&)的类型,都能作为std::for_each的迭代器参数。

typing.Protocol可视为python中的鸭子类型概念,当类型T实现了某一Protocol子类P中所有属性/方法时,注解才将T, P视作匹配的类型

class HasRunMethod(Protocol):
    def run(self) -> None:
        pass

class A:
    def run(self) -> None:
        print("running A")

class B:
    def lazy(self) -> None:
        pass
valid_runner: HasRunMethod = A()
invalid_runner: HasRunMethod = B()
# Expected type 'HasRunMethod', got 'B' instead

2. 容器

List[T]/Set[T]注解存储类型T的列表/集合

Tuple[T1, T2, T3, ..., TN]注解存储类型T1, T2, T3, ..., TN的元组

Dict[K, V]注解键类型K,值类型V的字典

typing内还有其他非内置容器类型,如NamedTuple, DefaultDict等(namedtuple是创建NamedTuple的工厂函数)

3.泛型

聚合类型Union的具体类型是始终不确定的,而泛型类型typing.TypeVar表示一种待确定的类型,静态类型检查器可以通过形参推导出T的具体类型

TypeVar创建模板参数,模板约束只能进行类型匹配或继承匹配(bound),无法进行语义上的类型约束(类似cpp中的SFINAEconcepts)。后者利用了不求值表达式,这在python语法中没有,所以或许该考虑组合TypeVarProtocol达成相同效果

一个泛型函数举例

from typing import List, TypeVar

T = TypeVar('T', int, float)
# only int or float is acceptable
def quick_sort(arr: List[T]) -> List[T]:
    if len(arr) <= 1:
        return arr
    pivot: T = arr[len(arr) // 2]
    left: List[T] = [x for x in arr if x < pivot]
    middle: List[T] = [x for x in arr if x == pivot]
    right: List[T] = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)
res = quick_sort([1, 2, 3])

Pycharm2023中,上文代码中res成功推导为list[int]

另外,让class继承typing.Generic[T1, T2, ..., Tn],可以实现模板参数为T1, T2, ..., Tn的类模板