📌 知识点简介
很多 Python 开发者对类型注解的认知停留在 name: str = "hello" 和 def add(a: int, b: int) -> int。但 typing 模块的 泛型、协变逆变、Protocol(结构子类型) 这些高阶特性,能让你写出既灵活又能通过严格类型检查的代码。今天聚焦三个高频场景:泛型函数 、Protocol(鸭子类型的形式化) 、和 TypedDict(结构化字典)。
🔧 核心代码
1. TypeVar:写一个「接受多种类型」的函数
python
from typing import TypeVar, List
T = TypeVar("T") # 类型变量,调用时自动推导
def first(items: List[T]) -> T | None:
"""返回列表的第一个元素,类型与列表元素类型一致"""
return items[0] if items else None
# 使用
x: int | None = first([1, 2, 3]) # 推导出 T = int
y: str | None = first(["a", "b"]) # 推导出 T = str
z: float | None = first([]) # 推导出 T = float,返回 None
边界约束:限制泛型参数的范围
python
from typing import TypeVar
# 只有 int 和 float 可以传入
Number = TypeVar("Number", int, float)
def double(n: Number) -> Number:
return n * 2
double(5) # ✅ 10
double(3.14) # ✅ 6.28
double("hi") # ❌ 类型检查报错
2. Protocol:鸭子类型的正式声明(结构子类型)
传统抽象(ABC/继承)要求显式继承,而 Protocol 只要求对象拥有特定的方法/属性------这才是 Python 的真精神。
python
from typing import Protocol
class Drawable(Protocol):
"""任何拥有 .draw() 方法的对象都是 Drawable"""
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "⚪ 画圆"
class Square:
def draw(self) -> str:
return "⬜ 画方"
class Car:
def run(self) -> str: # 没有 draw 方法
return "🚗 跑"
def render(obj: Drawable) -> None:
print(obj.draw())
render(Circle()) # ✅ 正确:Circle 有 draw 方法
render(Square()) # ✅ 正确:Square 有 draw 方法
render(Car()) # ❌ 类型检查报错:Car 没有 draw 方法
实战:log 接口(不依赖具体日志库)
python
from typing import Protocol
class Logger(Protocol):
def info(self, msg: str) -> None: ...
def error(self, msg: str) -> None: ...
def process_data(data: dict, logger: Logger) -> None:
logger.info(f"开始处理: {len(data)} 条")
# ... 业务逻辑 ...
logger.error("处理失败")
# 不需要继承:任何有 info/error 的对象都能用
import logging
process_data({"a": 1}, logging.getLogger("app")) # ✅ Python 自带的 logger 满足协议
优势 :你不再需要为单元测试 mock 写复杂的继承------只要对象满足协议签名就行。这也是 Iterable / Callable 等内置协议的工作原理。
3. TypedDict:给字典加类型(告别 **kwargs 乱接)
python
from typing import TypedDict, NotRequired # NotRequired: Python 3.11+
class UserInfo(TypedDict):
name: str
age: int
email: NotRequired[str] # 可选字段
def create_user(info: UserInfo) -> None:
print(f"用户: {info['name']}, 年龄: {info['age']}")
# ✅ 正确使用
create_user({"name": "Alice", "age": 30})
create_user({"name": "Bob", "age": 25, "email": "bob@example.com"})
# ❌ 类型检查报错:缺少 age 字段
create_user({"name": "Charlie"})
对比 @dataclass 和 TypedDict:
| 场景 | 用 dataclass | 用 TypedDict |
|---|---|---|
| 数据结构来自外部(JSON/API) | ❌ 需手动转换 | ✅ 天然匹配 |
| 需要方法逻辑 | ✅ | ❌ |
| 需要动态增减字段 | ❌ | total=False 支持 |
| ORM/Schema 定义 | 多用 | RPC 调用好用 |
python
# 与 JSON API 天然配合
class ApiResponse(TypedDict):
code: int
data: dict
message: str
def parse_response(resp: str) -> ApiResponse:
import json
return json.loads(resp) # ✅ 返回的类型被正确注解
🧪 使用示例:综合实战
用上今日三个知识点的实战场景------一个类型安全的配置加载器:
python
from typing import TypeVar, Protocol, TypedDict, Any
import json
# 1. TypedDict 定义配置结构
class DatabaseConfig(TypedDict):
host: str
port: int
username: str
password: str
class AppConfig(TypedDict):
debug: bool
database: DatabaseConfig
allowed_origins: list[str]
# 2. Protocol 定义配置源接口
class ConfigLoader(Protocol):
def load(self, path: str) -> dict[str, Any]: ...
# 3. TypeVar 写泛型反序列化
T = TypeVar("T")
def load_config(loader: ConfigLoader, path: str, model: type[T]) -> T:
raw = loader.load(path)
return model(**raw) # type: ignore # TyepDict ** 展开
测试用 Mock 直接传一个普通对象(不继承任何东西,完全靠 Protocol):
python
class MockLoader:
def load(self, path: str) -> dict:
return {"debug": True, "database": {"host": "localhost", "port": 5432, "username": "root", "password": ""}, "allowed_origins": []}
loader = MockLoader()
config = load_config(loader, "config.json", AppConfig)
print(config["debug"]) # True
⚠️ 注意事项 / 避坑指南
| 坑点 | 说明 | 解决 |
|---|---|---|
| ❌ 类型注解只在静态检查生效 | TypeVar 不会在运行时做类型校验,传入错误类型不会抛异常 |
加 isinstance 校验或使用 pydantic |
❌ TypeVar 绑定 vs 约束 |
bound=SomeClass = 必须是指定类的子类;int, str = 只能是这些具体类型 |
明确语义:子类用 bound,枚举用约束 |
| ❌ Protocol 的结构匹配是「安全的」但不是「完备的」 | 只要签名匹配就符合,不检查语义 | 做好注释文档,单元测试仍然重要 |
❌ TypedDict 的 ** 展开 |
model(**raw) 在类型检查时可能会报错,因为 TypedDict 不是真正的 dict |
加 # type: ignore 或使用 pydantic 模型 |
| ❌ NotRequired 需要 Python 3.11+ | 旧版本没有 | 3.8-3.10 可用 total=False 控制整个 TypedDict |
total=False 替代方案(兼容 Python 3.8+):
python
class PartialConfig(TypedDict, total=False):
debug: bool
host: str
port: int
# 所有字段都是可选的
📝 总结
TypeVar:让你的函数类型跟着输入类型走,告别Any污染Protocol:鸭子类型的正式身份证,解耦接口和实现,Mock 零侵入TypedDict:让 JSON/dict 数据有了精确的形状定义,API 对接更安全
很多人说 Python 的类型注解是「写了也改变不了运行时的行为」。确实如此,但它的真正价值在于:在你敲下
mypy或pyright检查的瞬间,就把一批潜在的 bug 从运行时前移到了编码时。 对于 3 个月后回来改代码的自己来说,好的类型签名就是最好的文档。