Python 类型检查 ---- typing 模块
文章目录
Tutorials
PEP 484:
- 简略介绍,PEP 483: https://www.python.org/dev/peps/pep-0483/
PEP 544
- Protocols: Structural subtyping (static duck typing)
- 用来规定鸭子类型的检验 typecheck
- 英文:https://www.python.org/dev/peps/pep-0544/
mypy 泛型教程
Subtype 概念(子类型)
type tree
类型树
- 类似 class tree (类树)
注意
严格使用问题
- 虽然 type checker 支持 type tree
- 但是 Python 语言本身,不按 type tree 来编写,也能正常运行
a = 1; –> a = 'abc'
特殊使用情况
- 只支持 子类型, 而不需要父类型
- 比如:number –> int, 实际我需要的合法内容是 int
List
List[int]
- List 只包含 int 类型
- eg: List[float]
Gradual Typing
用于只标注一部分代码
- 可以很好的平衡 动态类型和静态类型
is consistent with 类型(区别于相对 Subtye)
- 与 …… 一致类型
三个规范
A type t1 is consistent with a type t2 if t1 is a subtype of t2. (But not the other way around.)
- 子类型 对于 父类型 是 一致类型
Any is consistent with every type. (But Any is not a subtype of every type.)
- Any 类型 对于 所有类型 是一致类型
Every type is consistent with Any. (But every type is not a subtype of Any.)
- 所有类型 对于 Any 类型 都是一致类型
Any 类型
- 一种新类型,类型通用表示符号
表示的目标对象
- 所有的值
- 所有的方法
Types vs. Classes
Class 是 动态概念
- 使用 class 定义, 被 type(obj) 内置函数返回
types 是 静态概念,用于 static type checker
出现位置
- 变量,方法,类型声明
辨析
- Class 一定是 type
type 不一定是 Class
- eg: Union[str, int]
- 但是,type 接口是通过 Class 实现的
type 的特殊性
虽然是 Class,但是
- 不能被实例化
- 不能被继承
- 不能被 isinstance 使用
如果被 isinstance 或 issubclass 使用
- 报 TypeError 错误
Block 类型(组合类型)
Any
Union[t1, t2, …]
- 真正类型,可能是 t1, t2, … 它们的 Subtype 中的一个
Subtype 类型问题
根据类型枚举 集合判断
- 类型多的是 父类型 Union[int, float, str]
- 类型少的是 子类型 Union[int, float]
枚举类型排序
- 排序无关紧要
嵌套 Union 问题
- Union[int, Union[float, str]] 横等于 Union[int, float, str]
- 枚举类型完全被展开了
枚举类型之间是 Subtype 关系
- 只保留 父类型
枚举类型数量不能为空
- Union[] –> 非法
- Union[()] –> 非法
单个枚举类型
- Union[int] 等价于 t1
出现 object 类型
- Union[…, object, …] returns object
- 结果就是 object
Optional[t1]
- 即,Union[t1, None]
Tuple[t1, t2, …]
- tuple 类型
- t1: 第一个元素的类型
- t2: 第二个元素的类型
- eg: Tuple[int, float] –> (3, 5.27)
Tuple[()]
- 空 tuple,即:(,)
Tuple[t1, …]
- 类型都是 t1 的,可变长度 tuple
Subtype 类型
Tuple[u1, u2, …] 是 Tuple[t1, t2, … ] 的 父类型
前提条件
- u1, u2, … 分别是 t1, t2, … 的对应 父类型
Callable 类型
| |
[t1, t2, …, tn]
- 对应输入参数的类型
tr
- 对应返回值的类型
无参函数 与 不检查输入参数
| |
缺点
不能检查可变参数
- fun(*args)
- fun(**kwargs)
解决方法
- Callable[…, tr]
Generic Types 泛型
https://www.python.org/dev/peps/pep-0483/#generic-types
- Classes, that behave as generic type constructors are called generic types.
类似 Java, C++ 泛型
1 2 3 4 5 6 7def take_first(seq: Sequence[T]) -> T: # a generic function return seq[0] accumulator = 0 # type: int accumulator += take_first([1, 2, 3]) # Safe, T deduced to be int accumulator += take_first((2.7, 3.5)) # Unsafe
Generic Type Construct
- 接收一个类型,返回一个类型
- eg: typing.Iterable
Generic Function
即,使用 Generic Types 定义的函数
1 2 3 4 5def take_first(seq: Sequence[T]) -> T: # a generic function return seq[0] accumulator = 0 # type: int accumulator += take_first([1, 2, 3]) # Safe, T deduced to be int这里 type variale: T, 不需要有具体值
- 类似 C++, 在使用时才有意思
Type Variables 类型变量
非约束性 type variable
- 即:不说明限定类型
1X = TypeVar('X')特性
- 声明一个独特的 type variable
- 这里 变量名 和 TypeVar 中的参数,必须相同
- X –> 'X'
- Y –> 'Y'
约束性 type variable
- 即:带限定类型
1Y = TypeVar('Y', t1, t2, ...)- 作用类似 Union(t1, t2, …)
在函数参数中出现多次, 返回值类型 中出现 def fun(a: T, b: T) –> T: …
约束性 类型变量 type variable
各个参数的具体类型要完全一致
1 2 3 4 5 6 7 8S = TypeVar('S', str, bytes) def longest(first: S, second: S) -> S: return first if len(first) >= len(second) else second result = longest('a', 'abc') # The inferred type for result is str result = longest('a', b'abc') # Fails static type check注
这里表示 形参 first, second 的类型 和 return 的类型 完全一致
- 并不是 只要 是 S 中 的 (str, bytes) 任意一个就可以
eg:
- (str, str) –> str: 合法
- (bytes, bytes) –> bytes: 合法
(str, bytes) –> ? : 非法 - 不能判定返回值类型
非约束性 类型变量 type variable
对于输入参数 类型
- 没有约束性
对于返回值 类型
约束的是,合法类型为:公共子类
1 2 3 4 5 6 7 8S = TypeVar('S') def longest(first: S, second: S) -> S: return first if len(first) >= len(second) else second class MyStr(str): ... result = longest(MyStr('a'), MyStr('abc'))
Union 类型
对于输入参数,返回值 类型
- 都是约束宽泛
对于任意一个参数
- 类型只要是 Union 中的一个,就合法
注意
这里会检查,运算的合法性
1 2def concat(first: U, second: U) -> U: return x + y # Error: can't concatenate str and bytes
- 即:参数类型不同时,可能导致的代码,变成非法操作问题
声明和使用 Generic Types
https://docs.python.org/3/library/typing.html#user-defined-generic-types
声明
使用 class 定义 和 继承 Generic[X, Y, …] 类型 实现
注意
- 继承的 Generic Type
作用
用来包裹 定义类,需要的多个 TypeVar
即,一个 TypeVar 的 包裹器
类似 C++, std::map<char, int>
- 这里的<char, int>
typing.NewType
https://stackoverflow.com/a/58775376
只是用来创建一个类型标记
- 在 runtime 运行时,返回的还是原来类型
确切的说
- 它返回的是一个,类型标记函数
- 而不是一个类
- 因此,不能被继承
使检验生效问题
| |
只有使用了 你定义的 NewType–>UserId 时,才能触发 typecheck
也就是,只有确切使用 UserID(your_input)
这样才能被进行检验
1 2# typechecks user_a = get_user_name(UserId(42351))
不适用 UserId 包裹的参数
不会被检验
1 2# does not typecheck; an int is not a UserId user_b = get_user_name(-1)
当作原类型使用
| |
注意
上面的返回值 output,
- 是 int 类型,不是 UserId 类型
在制作 新的 NewType 时,当作参数使用
| |
typing.Iterable
typing 规定的类型
- Generic
- Any
- Tuple
List
- 一种 list 的 Generic 类型
注意
- 推荐使用在 返回值类型
而,在参数中,使用 Sequence 或者 Iterable
1 2 3 4 5 6 7T = TypeVar('T', int, float) def vec2(x: T, y: T) -> List[T]: return [x, y] def keep_positives(vector: Sequence[T]) -> List[T]: return [item for item in vector if item > 0]
- Callable
- Iterable
- Sequence
- Mapping
- Sized
NewType
- 类型辅助函数
鸭子类 Vs. 明确的子类型
https://docs.python.org/3/library/typing.html#nominal-vs-structural-subtyping
明确的子类型 explicit base classes
- PEP 484 中
在 类定义的开头
声明父类
1 2 3 4 5 6from typing import Sized, Iterable, Iterator class Bucket(Sized, Iterable[int]): ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[int]: ...
鸭子类型
PEP 544 中
- 即,遵循 class protocol
类的方法
接口与需要的类型相似,即可
1 2 3 4 5 6 7 8 9from typing 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
鸭子类,方法接口检验 typing.Protocol
python 3.8 特性 参考:
作用:
- 说明接收的类型,有给定的方法等接口
| |
这里 func(x: Proto)
用来检验 实参 x
- 是否使用了 Proto 规定的方法接口
当成 Generic 泛型使用
| |
运行时使用 @typing.runtime_checkable
作用:
- 把 Protocol 子类标注为,在 runtime 时也可以使用
例子:
| |
变量的检查
格式
在注释中说明类型
1 2 3 4d = dict() # type: Dict[str, int] d['id'] = 3 d['id'] = 1.2 # --> 这里会报错,不合法冒号形式
1 2 3 4from typing import List members: List[str] = ['Lily', 'Lucy']
泛型之 协变、逆变、不变
- invariant 不变、covaraint 协变、contravariant 逆变
- 这是 java, c#, python 等,都有的泛型概念
教程
Python 中
不变
大部份 可变类型 都属于不变类型
- List, Dict 类型 就属于不变类型
eg: List[Animal] 不允许 List[Cat]
- 这里 List[Animal] 只能接收 Animal 本身
因为 your_list[index] 会 按预先设定的 类型 进行检查
1 2 3 4 5 6 7 8 9 10 11 12 13class Shape: pass class Circle(Shape): def rotate(self): ... def add_one(things: List[Shape]) -> None: things.append(Shape()) my_things: List[Circle] = [] add_one(my_things) # This may appear safe, but... my_things[0].rotate() # ...this will fail
内置不变类型 –> 协变变类型
- 与 typing 模块中 的 泛型类型 版本关系
- list –> List –> Sequence
- dict –> Dict –> Mapping
| builtins | 不变版本 | 可变版本 |
|---|---|---|
| list | List | Sequence |
| dict | Dict | Mapping |
协变
大部份用户自定义的 泛型类型属于协变类型
- Union 属于协变类型
- eg: Union[Animal] 允许 Union[Cat]
手动设定,协变还是逆变
使用 TypeVar('your_name', covariant=True)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15from typing import Generic, TypeVar T_co = TypeVar('T_co', covariant=True) class Box(Generic[T_co]): # this type is declared covariant def __init__(self, content: T_co) -> None: self._content = content def get_content(self) -> T_co: return self._content def look_into(box: Box[Animal]): ... my_box = Box(Cat()) look_into(my_box) # OK, but mypy would complain here for an invariant type
Java 和 概念解释
协变
意思
- 允许子类
java
1<? extends T>
逆变
意思
允许父类
1<? super T>
用法 与 作用
用来限定 容器能放入的类型
1 2 3 4List<? super Number> list001 = new ArrayList<Number>(); List<? super Number> list002 = new ArrayList<Object>(); list001.add(new Integer(3)); list002.add(new Integer(3));解释
上面 List<? super Number> 限定了容器类型
- 至少是 Number 类,或者 Number 类的父类
这样我们就可以
- 安全的在 容器中(list001),放入 Number 类的 子类类型的元素了
不变
意思
- 只允许当前类型
java
1T
TypeGuard
python 3.10 特性
概念:
typeguard : 它是指 python 的一些类型判断语句或函数
例子:简单例子
1 2 3 4 5 6 7 8def func(val: Optional[str]): # "is None" type guard if val is not None: # Type of val is narrowed to str ... else: # Type of val is narrowed to None ...解说:
- 这里
val is not None和isinstance(val, str)进行类型缩窄(type narrowing) - 这就是一种 typeguard
- 注:这种语句能够被 mypy 识别
- 这里
例子:自定义函数类型检验
1 2 3 4 5 6 7def is_str_list(val: List[object]) -> bool: """Determines whether all objects in the list are strings""" return all(isinstance(x, str) for x in val) def func1(val: List[object]): if is_str_list(val): print(" ".join(val)) # Error: invalid type解说:
- 这里函数
is_str_list()检验一个列表是否是 List[str] - 这个函数也是一种 typeguard
- 注:这种自定义函数作为 typeguard 不能被被 mypy 识别
- 这里函数
解说:
- 因此,typing.TypeGuard 引入了一种功能,让 mypy 也能识别自定义函数类型的
类型检验工具(typeguard)
使用例子:
| |
注释:
这里把
-> bool写成了TypeGuard[List[str]]- 意为:检验数据类型是否是 List[str] 类型
例子:检验一个 class
| |
- 注释: 这里使用了
"TypeGuard[Person]"
特点
- 一个 TypeGuard 自定义函数,总是返回 bool 类型
检验值 value,总是第一个位置参数
1 2def my_guard(value: InputType, ...) -> TypeGuard[CheckType]: ...- 检验的类型 CheckType 总是比输入类型 InputType 严谨
注意
自定义 TypeGuard 使用时,不可使用
is True1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22from __future__ import annotations from typing import Any, TypeGuard class MyPositive: def __init__(self, value): self.value = value def is_my_positive(num: Any) -> TypeGuard[MyPositive]: return isinstance(num, MyPositive) def foo(val: None | MyPositive): # 正确用法 if is_my_positive(val): return val.value # 错误用法,mypy 报错 if is_my_positive(val) is True: return val.value- 验证:
lsp-bridge + pyright
- 验证:
TypeAlias
python 3.10 特性
参考:
作用:
- 用来区别赋值类别名类型
- 告诉 mypy, 它是一个类型别名,不是一个 class 类对象
explicit syntax, 显式说明类型别名
1 2mytype = str # 赋值,类对象,mytype 是一个普通变量,implict alias_mytype: TypeAlias = str # 类型别名, explicit
例子:
| |
ParamSpec
python 3.10 特性
参考:
作用:
- 用来注解回调函数 callback 和装饰器 decorator 等高级函数用法
例子:
| |
解释:
这里 P 用来注解整个函数参数表
Callable[P, T]- P 是参数表
- T 是返回值
- 获取位置参数方法: P.args
获取关键字参数方法: P.kwargs
- 传递给内部函数参数,就不能直接用 P,只能用 P.args 和 P.kwargs 了
Concatenate
python 3.10 特性
作用:
- 和 Callable、ParamSpec 一起使用用来注解高级函数用法
- 和 ParamSpec 一起使用,拼接函数参数表
特点:
只能用在函数参数表的开头
例子:
| |
解说:
Callable[Concatenate[Lock, P], R]
- 这里 Concatenate[Lock, P]: 拼接了函数参数表
- 表示 Lock 和 P 中包含的参数一起构成了这个函数的参数表
Type
参考:
注:
- 已经从 python 3.9 开始 deprecated, 推荐使用 builtin.type 代替
作用:
- 类似 type(val), 获取一个变量的类型
例子:
| |
解说:
- a 具有 int 类的类型,即 type(int) 类型, 这里规定为 Type[int] 类型
Literal
python 3.8 特性
参考:
作用:
- 把子面量本身作为一个类型
例子:
| |
解说:
Literal[True]: 只接受 True 作为合法值- 即,它把 True 字面量作为了一个类型注解
ClassVar
参考:
作用:
用来说明一个变量是类变量
- 有些类似
@classmethod功能
- 有些类似
- 禁止实例变量访问它
例子:
| |
Final
python 3.8 特性
作用:
- 声明,变量是常量
| |
@final 装饰器
python 3.8 特性
作用:
- 禁止方法重写
- 禁止类继承
| |
Annotated
python 3.9 特性 参考:
作用:
- 除类型之外,提供额外注解信息
特点:
这个额外信息处理方式:
- 在 static analysis 和 runtime (静态分析和运行时)都可以使用
但是,怎么处理,由处理给定变量的工具 tool 或 library 决定
- 没有处理功能,就会被忽略
- 额外信息,可以有多个
额外信息像 list 一样
- 有顺序
- 可重复
- 可嵌套
例子:
| |
TypedDict
python 3.8 特性
参考:
作用:
- 对
声明的 key对应value的类型做限定
特点:
- 对没有声明的 key 对应的 value 类型无影响
用法:
两种模式
定义一个类
- 支持继承
- 通过函数创建
| |
忽略类型检查 ignore
参考:
| |
文章作者 Sawyer Zheng
上次更新 2024-07-16 (7f33ae8)