跳到主要内容

Python 类型提示实战指南

类型提示的核心作用是明确接口契约,从而支持静态分析、编辑器补全及重构。需要明确的是,Python 运行时不会强制执行函数或变量的类型注解。对于不可信的外部输入,必须单独进行运行时验证。

from collections.abc import Iterable

def average(values: Iterable[float]) -> float:
numbers = list(values)
if not numbers:
raise ValueError("values must not be empty")
return sum(numbers) / len(numbers)

注解策略建议: 优先为公共 API 边界和关键内部模型添加注解。对于静态检查器(如 mypy, pyright)能够自动推断的局部变量,通常无需显式标注。

锁定 Python 版本

类型语法随 Python 版本演进。务必在项目配置中声明最低支持版本,并让静态检查器配置与之保持一致。若需使用较新版本的类型特性但需兼容旧版本运行时,请使用 typing_extensions 进行回填。

Python 3.10+ 推荐写法: 优先使用内置泛型容器和 | 联合类型语法:

def lookup(names: list[str], fallback: str | None = None) -> str: ...

注意: type 语句(类型别名)和方括号泛型参数语法(如 class Box[T])需要 Python 3.12+。若运行时目标低于 3.12,请回退使用传统的 TypeAliasTypeVar 形式。

建模语义,而非存储细节

类型注解应反映数据的用途行为,而非其底层存储结构:

  • 接受抽象能力:若函数仅遍历数据,接受 IterableSequenceMapping 等抽象基类,而非具体的 listdict
  • 返回具体类型:若调用方依赖特定容器的行为(如 list 的索引访问),则返回具体类型。
  • 有限值集合:对于少量且有明确含义的固定值,使用 Literal 或枚举(Enum)。
  • 结构化数据
    • 字典形状的记录:使用 TypedDict
    • 带有行为或不变量的值:使用 dataclass 或普通类。
  • 结构化接口:使用 Protocol 定义鸭子类型接口,无需强制继承。
  • 慎用 Any:将 Any 限制在无类型边界(如第三方库接口),并尽快收窄类型。Any 意味着“跳过检查”,而非“未知但安全”。
from typing import Protocol

class SupportsClose(Protocol):
def close(self) -> None: ...

def finish(resource: SupportsClose) -> None:
resource.close()

类型收窄与空值处理

处理联合类型(Union)时,应依赖真实的运行时证据进行收窄:

def length(value: str | bytes | None) -> int:
if value is None:
return 0
if isinstance(value, bytes):
return len(value)
# 此处 value 被收窄为 str
return len(value)

常见误区:

  • Optional[T] 等价于 T | None,它意味着参数有默认值。
  • 避免用 cast 替代检查。cast 仅改变静态检查器的视角,不会在运行时产生任何效果,若逻辑错误,cast 会掩盖问题。

运行时边界

不同 Python 版本中,注解的表示和求值方式可能存在差异。若代码需要内省(introspect)注解,应使用文档化的 API(如 typing.get_type_hints),并注意处理导入、前向引用(forward references)及潜在的求值副作用。

推荐的类型工作流:

  1. 在 CI 中运行配置好的静态检查器。
  2. # type: ignore 等抑制指令视为狭窄的、有文档记录的例外。
  3. 独立测试运行时行为。

核心观点: 静态检查器通过不等于代码正确。它仅证明未发现某一类契约不匹配。

参考资源

探索关联

这篇笔记还没有文档关联。

同主题的其他笔记 (46)

打开关联网络