现代类型系统:Union、Protocol 与泛型
目标:能够把类型标注用作静态设计工具,正确选择具体类型、抽象容器、Protocol、类型参数、object 与 Any。
Python 类型标注不会自动检查运行时参数。它们主要供 Pyright、mypy、IDE 和重构工具使用。这一点和 Rust、Go 的编译期类型系统根本不同:Python 程序可以在静态检查失败后继续运行,也可以在没有 checker 时完全忽略标注。
但“可选”不等于“装饰”。好的标注记录数据流关系,并在执行前发现一整类接口错误。
object 不等于 Any
所有值都能赋给 object,但你必须先缩窄类型才能做特定操作;Any 则告诉 checker 跳过检查,并会把不确定性传播出去。边界接收未知数据时优先 object,第三方无标注且暂时无法封装时才局部使用 Any。
Protocol 是静态 duck typing
Go interface 要求方法集合匹配;Python Protocol 也按结构判断,不要求实现类继承 Protocol。它很适合描述 provider、repository、serializer 这类小能力。
完整示例:可替换的文本提供者
保存为 typing_lab.py:
from __future__ import annotations
from collections.abc import Iterable, Sequence
from typing import Protocol
class TextProvider(Protocol):
def summarize(self, text: str) -> str:
raise NotImplementedError
class LocalProvider:
def summarize(self, text: str) -> str:
return text.strip()[:12]
def summarize_all(provider: TextProvider, texts: Iterable[str]) -> list[str]:
return [provider.summarize(text) for text in texts]
def first[T](items: Sequence[T]) -> T:
if not items:
raise ValueError("items must not be empty")
return items[0]
if __name__ == "__main__":
result = summarize_all(LocalProvider(), [" alpha message ", "beta"])
print(result)
assert first(result) == "alpha messag"
运行:
uv run python typing_lab.py
预期输出:
['alpha messag', 'beta']
LocalProvider 没有继承 TextProvider,却满足方法签名。first[T] 使用 Python 3.12 的类型参数语法,保留“输入元素类型等于返回类型”的关系;若写成返回 object,调用者会丢失信息。
Union 与缩窄
str | None 表示两个真实状态。使用前通过 is None 缩窄:
def length(value: str | None) -> int:
if value is None:
return 0
return len(value)
不要写 dict | list | object;因为 union 中的 object 已覆盖所有对象,其他成员不增加约束。
引导实验:让错误先在 checker 中出现
暂时加入错误类:
class BrokenProvider:
def summarize(self, text: bytes) -> bytes:
return text
summarize_all(BrokenProvider(), ["hello"])
Python 运行到 provider 调用时才可能失败或返回错误类型;静态 checker 应在执行前指出方法签名不兼容。第 11 课会正式配置 Pyright,现在先理解它检查的是什么。
独立练习
定义泛型函数 last[T](items: Sequence[T]) -> T,并增加 JsonProvider,其 summarize(str) -> str 返回 JSON 字符串。不要让 Provider 返回 Any。
参考实现
import json
from collections.abc import Sequence
def last[T](items: Sequence[T]) -> T:
if not items:
raise ValueError("items must not be empty")
return items[-1]
class JsonProvider:
def summarize(self, text: str) -> str:
return json.dumps({"summary": text.strip()[:12]}, ensure_ascii=False)
provider: TextProvider = JsonProvider()
result = summarize_all(provider, ["hello"])
assert last(result) == '{"summary": "hello"}'
常见问题
- checker 提示 Protocol 不匹配:比较参数名、参数类型、同步/异步和返回类型,而不只看方法名。
- 到处加入
Any后错误消失:检查并未“修好”,只是被关闭。 - 类型参数增加了复杂度却没有关系可保留:返回类型固定时普通函数就够了。