跳转到内容
新建笔记

Python 类型注解:静态约束、运行时对象与 typing

注解说明预期,运行时校验另行实现

跳转到“注解说明预期,运行时校验另行实现”

path: str 表示参数按字符串使用,-> np.ndarray 表示预期返回 NumPy 数组。Python 不会仅因这些注解,就在每次调用时检查参数、转换类型或验证数组尺寸。注解主要供读者、编辑器和静态类型检查器使用;运行时对象仍按实际操作执行。

本文以 Python 3.11、NumPy 2.4 为示例范围,将原点云函数的注解说明与 typing 名称表整理在一起。解释器执行与 mypy 1.18.2 的静态检查分别验证,不把“代码跑通”当作“类型检查通过”。

下面刻意传入一个不符合注解的值。保存为 annotation_boundary.py,直接运行可以通过断言;静态检查应报告调用参数类型错误。

def keep(value: int) -> int:
return value
result = keep("runtime is still a string")
assert isinstance(result, str)
print(result)

运行时没有自动强制转换,所以字符串仍是字符串。这个结果不表示注解没有用途:在调用链很长时,检查器可以在执行之前发现约定不一致。反过来,即使静态检查通过,文件是否存在、数值是否有限、数组是否为三列,也未必已经得到验证。Python 3.11 typing、mypy 的检查方式

常用类型:容器、缺省参数与泛型

跳转到“常用类型:容器、缺省参数与泛型”
表达方式含义容易混淆的地方
list[int]、dict[str, float]元素/键值类型约定不等于运行时遍历验证所有内容
tuple[int, str]固定两个位置,各有自己的类型与任意长度同类型元组 tuple[int, ...] 不同
int | None允许整数或 None不代表这个参数一定可以省略;可省略还取决于默认值
Sequence[T]按序列接口读取 T不承诺是 list,也不提供可变操作的类型约定
TypeVar("T")关联多处出现的同一类型参数不只是把每个位置都写成 Any
Any放宽静态约束的特殊类型容易让错误继续传播,不自动形成运行时检查

Python 3.9 起可在注解中直接使用 list[int] 等内置泛型;3.10 起可以写 X | Y。以下使用 3.11 可用的 TypeVar 风格,不使用 3.12 才引入的 type 语句与新类型参数语法。PEP 585、PEP 604

from collections.abc import Callable, Sequence
from typing import TypeVar, get_type_hints
T = TypeVar("T")
def first_or_none(values: Sequence[T]) -> T | None:
return values[0] if values else None
def describe(value: int, formatter: Callable[[int], str]) -> str:
return formatter(value)
def format_number(value: int) -> str:
return f"value={value}"
assert first_or_none([1, 2]) == 1
assert first_or_none(("a", "b")) == "a"
assert first_or_none([]) is None
assert describe(3, format_number) == "value=3"
assert get_type_hints(format_number) == {"value": int, "return": str}
print("generic sequence and callable annotations passed")

get_type_hints 在这里仅解析本程序自己定义的注解,用来观察约定,不用来检查实际调用值。注解解析可能求值名字或前向引用,不能把读取任意外部对象注解当作无条件无副作用的验证器。

点云例子:dtype 注解仍没有验证 N×3

跳转到“点云例子:dtype 注解仍没有验证 N×3”

原函数的默认路径为 thread_green/point_cloud.ply,通过 o3d.io.read_point_cloud(path) 得到点云,再执行 np.asarray(pcd.points) 返回顶点数组。path: str 和 -> np.ndarray 都只是约定;它们没有证明文件存在、点云非空、数据有限,或返回数组恰好有三列。Open3D 的 NumPy 互操作还涉及底层数据共享,不能把 np.asarray 一概理解为独立副本。Open3D 点云读取、Open3D 与 NumPy

下面单独展示一个可运行的数组边界函数,不依赖 Open3D 或外部 PLY 文件。它接收可转为数组的值,转成 float64,要求非空 N×3 且全部有限,并返回独立副本。

import numpy as np
import numpy.typing as npt
def validate_xyz(points: npt.ArrayLike) -> npt.NDArray[np.float64]:
values = np.asarray(points, dtype=np.float64)
if values.ndim != 2 or values.shape[1] != 3 or values.shape[0] == 0:
raise ValueError("expected a nonempty N-by-3 array")
if not np.isfinite(values).all():
raise ValueError("coordinates must be finite")
return values.copy()
source = np.array([[0., 1., 2.], [3., 4., 5.]])
points = validate_xyz(source)
assert points.shape == (2, 3) and points.dtype == np.float64
assert not np.shares_memory(points, source)
source[0, 0] = 99
assert points[0, 0] == 0
try:
validate_xyz([[1., 2.]])
except ValueError:
print("wrong column count rejected")
else:
raise AssertionError("shape was not checked")

NDArray[np.float64] 在这里约束 dtype 类型,未将具体形状写进类型系统,所以代码仍须检查 N×3。转换过程也可能因为字符串或不规则嵌套序列失败;空数组与非有限值则由自己的规则明确拒绝。本例只验证数组边界,不宣称运行过原 Open3D 文件读取链路。NumPy 类型支持

TypedDict、NamedTuple、NewType、ClassVar 与 Protocol

跳转到“TypedDict、NamedTuple、NewType、ClassVar 与 Protocol”

这些名字分别解决字典字段、元组字段、静态身份区分、类属性约定和结构接口等问题,不是同一种“运行时类型容器”。

from typing import (ClassVar, NamedTuple, NewType, NotRequired,
Protocol, TypedDict, runtime_checkable)
UserId = NewType("UserId", int)
class Record(TypedDict):
id: UserId
name: str
note: NotRequired[str]
class Point(NamedTuple):
x: float
y: float
@runtime_checkable
class HasName(Protocol):
name: str
class User:
category: ClassVar[str] = "person"
def __init__(self, name: str) -> None:
self.name = name
def display(user: HasName) -> str:
return user.name.upper()
raw_id = 1000
identifier = UserId(raw_id)
record: Record = {"id": identifier, "name": "Ada"}
point = Point(1.0, 2.0)
user = User(record["name"])
assert identifier is raw_id
assert type(record) is dict
assert point.x == 1.0 and tuple(point) == (1.0, 2.0)
assert isinstance(user, HasName) and display(user) == "ADA"
assert User.category == "person"
print(record, point, user.category)

TypedDict 的实例仍是普通字典,字段注解不自动校验每次写入;NamedTuple 则产生带命名字段的元组类型。NewType 在静态检查时区分身份,运行时返回传入对象,所以例中的 identifier is raw_id 成立。ClassVar 表达静态约定,不会单靠注解阻止实例赋值。PEP 589:TypedDict、PEP 526:变量与 ClassVar 注解

结构化 Protocol 不要求实现者显式继承它。加上 runtime_checkable 后,isinstance 只进行受限的运行时成员检查,不验证成员值类型和完整签名;不能把这个结果当作对象满足所有行为约定的证明。PEP 544:Protocol

原名称表:保留主题,纠正归属和示例

跳转到“原名称表:保留主题,纠正归属和示例”

原表共有 65 个名字,其中一部分属于 typing,一部分来自 abc、collections、collections.abc、types 或 re。下表保留这些查阅入口,避免把接口类当作现成可调用对象,或把普通 Python 函数误判为内置描述符。

常见具体容器与兼容别名

跳转到“常见具体容器与兼容别名”
原名称Python 3.11 中的查找或写法说明
List、Dict、Tuple、FrozenSetlist[T]、dict[K, V]、tuple[...]、frozenset[T]旧 typing 别名可识别,现代内置写法更直接
Deque、DefaultDictcollections.deque[T]、collections.defaultdict[K, V]创建对象要使用对应具体容器
ChainMap、Counter、OrderedDictcollections 中的同名类它们有各自运行时容器行为
NamedTupleclass Point(NamedTuple): x: float与 collections.namedtuple(...) 的动态创建方式区分
Match、Patternre.Match[str]、re.Pattern[str]字符串与 bytes 模式须匹配输入类型
Typetype[Base]描述类对象,不是把 type(str) 当作类型注解示例

这些名称的运行时来源与接口以 collections、re 为准。注解中的旧别名与运行时构造器不要混为一谈。

接口、迭代与上下文管理

跳转到“接口、迭代与上下文管理”
原名称对应接口与注意点
AbstractSettyping.AbstractSet[T] 对应集合接口;collections.abc 中名为 Set,没有同名 AbstractSet 导入
Collection、Container集合接口组合,与只要求成员包含操作的接口不同
Iterable、Iterator可迭代与迭代器;接口名本身不是可以直接遍历的数据
Generator注解分别涉及 yield、send 和 return 类型,不只列出一个元素类型
Sequence、MutableSequence可读取的序列接口与可变序列接口
Mapping、MutableMapping映射接口;现代 ABC 从 collections.abc 导入
MutableSet、Reversible可变集合与可反向迭代接口
Hashable、Sized哈希能力与长度协议,不能仅凭变量外观判断
MappingView、ItemsView、KeysView、ValuesView字典视图接口,实际对象可由字典的方法取得
CallableCallable[[参数类型...], 返回类型] 描述调用约定,不等于任意函数都会匹配
ByteString旧字节序列接口;应明确实际接受 bytes、bytearray 或其他缓冲区对象
IO、TextIO、BinaryIO文件对象注解;具体对象仍由 open 等接口创建
ContextManager、AsyncContextManager上下文管理协议;with ContextManager() 不是可用资源示例
Awaitable、Coroutine可等待对象与协程的接口;应由真实异步操作产生对象
AsyncIterable、AsyncIterator、AsyncGenerator异步迭代协议;需要实现相应异步方法或使用异步生成器

主要 ABC 位于 collections.abc;上下文管理的抽象基类位于 contextlib。ABC 的运行时检测也有边界,不能仅用一次 isinstance 证明对象的全部语义正确。

类型系统中的专用名字

跳转到“类型系统中的专用名字”
原名称含义与修正
Any放宽静态检查;不等于一个自动验证任意类型的构造器
AnyStr受 str/bytes 约束的类型变量,常用于要求相关参数类型一致,不等价于任意混合 Union
Union、Optional联合类型;Optional[T] 表示 T 或 None,不直接决定参数是否有默认值
TypeVar、Generic类型参数及使用它的泛型类/函数关系
ClassVar类属性注解应写在具体字段上,如 count: ClassVar[int] = 0
TypedDict字段用 x: int 注解,不能用 x = int 冒充字段定义
NewType建立静态身份区分,运行时不创建原值的独立副本
NoReturn函数不会正常返回,例如总是抛异常;普通无返回值函数应注解为 None
Protocol结构接口约定;运行时检查与静态签名检查不同

有关参数化类型,isinstance(value, list[int]) 也不是运行时列表元素检查器,会引发 TypeError。需要验证每个元素时,必须实际编写相应逻辑,或者采用明确承担校验职责的库。泛型别名与运行时检查限制

数值协议与不属于 typing 的名字

跳转到“数值协议与不属于 typing 的名字”
原名称正确查找方向
SupportsAbs、SupportsRound__abs__、__round__ 等操作协议
SupportsBytes、SupportsComplex__bytes__、__complex__ 协议
SupportsFloat、SupportsInt、SupportsIndex__float__、__int__、__index__ 不是同一约定;索引语义更严格
ABCMetaabc.ABCMeta,用于抽象基类机制
MethodDescriptorTypetypes 中的内置方法描述符类型,如 str.join
MethodWrapperTypetypes 中的方法包装器类型,如 object().__str__
WrapperDescriptorTypetypes 中的包装器描述符类型,如 object.__init__
import abc
import types
import operator
assert isinstance(str.join, types.MethodDescriptorType)
assert isinstance(object().__str__, types.MethodWrapperType)
assert isinstance(object.__init__, types.WrapperDescriptorType)
class IndexValue:
def __index__(self) -> int:
return 2
class AbstractReader(metaclass=abc.ABCMeta):
@abc.abstractmethod
def read(self) -> str:
raise NotImplementedError
class Reader(AbstractReader):
def read(self) -> str:
return "ready"
assert operator.index(IndexValue()) == 2
assert Reader().read() == "ready"
print("descriptor kinds and explicit protocols passed")

普通 Python 类中 def method(...): ... 定义的函数与这些内置描述符不是同一种运行时类型。查阅时先确认名字的所属模块、是运行时对象还是注解构造,再判断如何使用。types 类型名称、abc 抽象基类、operator.index