注解说明预期,运行时校验另行实现
跳转到“注解说明预期,运行时校验另行实现”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, Sequencefrom 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]) == 1assert first_or_none(("a", "b")) == "a"assert first_or_none([]) is Noneassert 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 npimport 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.float64assert not np.shares_memory(points, source)source[0, 0] = 99assert points[0, 0] == 0try: 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_checkableclass 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 = 1000identifier = UserId(raw_id)record: Record = {"id": identifier, "name": "Ada"}point = Point(1.0, 2.0)user = User(record["name"])assert identifier is raw_idassert type(record) is dictassert 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、FrozenSet | list[T]、dict[K, V]、tuple[...]、frozenset[T] | 旧 typing 别名可识别,现代内置写法更直接 |
Deque、DefaultDict | collections.deque[T]、collections.defaultdict[K, V] | 创建对象要使用对应具体容器 |
ChainMap、Counter、OrderedDict | collections 中的同名类 | 它们有各自运行时容器行为 |
NamedTuple | class Point(NamedTuple): x: float | 与 collections.namedtuple(...) 的动态创建方式区分 |
Match、Pattern | re.Match[str]、re.Pattern[str] | 字符串与 bytes 模式须匹配输入类型 |
Type | type[Base] | 描述类对象,不是把 type(str) 当作类型注解示例 |
这些名称的运行时来源与接口以 collections、re 为准。注解中的旧别名与运行时构造器不要混为一谈。
接口、迭代与上下文管理
跳转到“接口、迭代与上下文管理”| 原名称 | 对应接口与注意点 |
|---|---|
AbstractSet | typing.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 | 字典视图接口,实际对象可由字典的方法取得 |
Callable | Callable[[参数类型...], 返回类型] 描述调用约定,不等于任意函数都会匹配 |
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__ 不是同一约定;索引语义更严格 |
ABCMeta | abc.ABCMeta,用于抽象基类机制 |
MethodDescriptorType | types 中的内置方法描述符类型,如 str.join |
MethodWrapperType | types 中的方法包装器类型,如 object().__str__ |
WrapperDescriptorType | types 中的包装器描述符类型,如 object.__init__ |
import abcimport typesimport 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()) == 2assert Reader().read() == "ready"print("descriptor kinds and explicit protocols passed")普通 Python 类中 def method(...): ... 定义的函数与这些内置描述符不是同一种运行时类型。查阅时先确认名字的所属模块、是运行时对象还是注解构造,再判断如何使用。types 类型名称、abc 抽象基类、operator.index