Course Builder个人学习工作台 · 本地模式
Modern Python:面向有经验开发者的回归路线

现代类型系统: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 后错误消失:检查并未“修好”,只是被关闭。
  • 类型参数增加了复杂度却没有关系可保留:返回类型固定时普通函数就够了。

参考资料