一、基本类型
使用python的标准库typing(Python3.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
注解函数返回值类型,标识函数不能通过正常控制流返回,所以函数体若只有return或pass,也是类型错误的,因为会隐式地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)
无返回值函数
尾置类型为None或type(None)注解一个无返回值函数(因为隐式返回None),当然None是一个类型为NoneType的单例对象,不是真正的无返回值,不能对位于c/c++中的void。以下代码可通过mypy检查
def get_none() -> None:
return None
可空类型
使用typing.Optional[T]注解一个可空类型(常用于形参、返回值),其类型为None或T,即等效于Union[None, T],对位于csharp中的可空值类型(Nullable<T>),c++17中的std::optional<T>。使用Optional[T]作为函数返回值,可以提醒调用者处理空值None
动态类型
typing.Any不约束类型,也就是python本质的动态类型,使用typing.Any时,优先考虑Union或TypeVar泛型注解平替,因为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在注解中相当于type,type也是一个内置类,且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中的SFINAE或concepts)。后者利用了不求值表达式,这在python语法中没有,所以或许该考虑组合TypeVar与Protocol达成相同效果
一个泛型函数举例
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的类模板