文章目录
-
- 从类型注解的局限说起
- 泛型:类型参数化的艺术
-
- 问题的根源:重复的类型签名
- [TypeVar 的三种形态](#TypeVar 的三种形态)
-
- [形态一:无约束 TypeVar------完全泛化](#形态一:无约束 TypeVar——完全泛化)
- [形态二:约束 TypeVar------限定类型范围](#形态二:约束 TypeVar——限定类型范围)
- [形态三:上界 TypeVar------必须是某个类型的子类](#形态三:上界 TypeVar——必须是某个类型的子类)
- 泛型类:类型参数化的容器
- [Python 3.12+ 的新语法:PEP 695](#Python 3.12+ 的新语法:PEP 695)
- Protocol:结构化子类型
-
- 从「继承接口」到「满足协议」
- [Protocol 的基本用法](#Protocol 的基本用法)
- [Protocol 的类型检查规则](#Protocol 的类型检查规则)
- 协议属性
- [可初始化协议:`init` 约束](#可初始化协议:
__init__约束)
- [泛型 + Protocol:类型安全的最高境界](#泛型 + Protocol:类型安全的最高境界)
- 工程实战:插件系统的类型安全架构
- [Protocol vs ABC:何时用哪个](#Protocol vs ABC:何时用哪个)
-
- 核心区别
- [什么时候用 Protocol](#什么时候用 Protocol)
- [什么时候用 ABC](#什么时候用 ABC)
- [`@runtime_checkable` 的陷阱](#
@runtime_checkable的陷阱)
- 高级模式:递归类型与自引用泛型
-
- [递归 Protocol:树形结构](#递归 Protocol:树形结构)
- 自引用泛型:流畅接口模式
- [Protocol 与 `@overload` 的配合](#Protocol 与
@overload的配合) - 常见陷阱与避坑
-
- [陷阱一:Protocol 的方法必须是抽象的](#陷阱一:Protocol 的方法必须是抽象的)
- [陷阱二:Protocol 不能用作基类](#陷阱二:Protocol 不能用作基类)
- [陷阱三:检查 Protocol 兼容性时忽略属性](#陷阱三:检查 Protocol 兼容性时忽略属性)
- 陷阱四:泛型协变误用
- [选型决策树:名义子类型 vs 结构子类型](#选型决策树:名义子类型 vs 结构子类型)
- 最佳实践总结
-
- 泛型最佳实践
- [Protocol 最佳实践](#Protocol 最佳实践)
- [泛型 + Protocol 的组合模式](#泛型 + Protocol 的组合模式)
- 小结
真正的类型安全不是"每个类都继承一个接口",而是"只要长得像鸭子,就是鸭子"------Protocol 让鸭子类型获得了静态检查的能力。
从类型注解的局限说起
在 类型注解进阶:Union、Optional、Any 与 Callable 中,梳理了 Union、Optional、Any 和 Callable 四个核心类型构造器。但留了两个关键问题没有解答:
- 如何写出类型安全的通用函数? ------
list[int]和list[str]共享相同的结构,但list本身不够精确,需要一种方式表达"元素类型可变,但行为固定" - 如何描述"有这个方法就行"的接口? ------
Callable只能描述函数签名,无法描述"有.close()方法"这样的协议约束
这两个问题的答案分别是**泛型(Generics)**和 Protocol(结构化子类型)。
泛型:类型参数化的艺术
问题的根源:重复的类型签名
没有泛型时,每种元素类型都需要一个独立的类型签名:
python
def first_int(items: list[int]) -> int:
if not items:
raise ValueError("empty list")
return items[0]
def first_str(items: list[str]) -> str:
if not items:
raise ValueError("empty list")
return items[0]
# 逻辑完全相同,只是类型不同
泛型用一个**类型变量(TypeVar)**来参数化:
python
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
"""返回列表的第一个元素,类型与列表元素类型一致"""
if not items:
raise ValueError("empty list")
return items[0]
# mypy 自动推断 T 的具体类型
reveal_type(first([1, 2, 3])) # int
reveal_type(first(["a", "b"])) # str
reveal_type(first([True, False])) # bool
TypeVar 的三种形态
形态一:无约束 TypeVar------完全泛化
python
from typing import TypeVar
T = TypeVar("T")
def identity(value: T) -> T:
"""返回与输入完全相同类型的值"""
return value
reveal_type(identity(42)) # int
reveal_type(identity("hello")) # str
reveal_type(identity([1, 2])) # list[int]
无约束 TypeVar 的关键规则:输入和输出的 T 必须是同一个类型。
python
T = TypeVar("T")
def double(value: T) -> T:
# ❌ 这不合法------如果 T 是 int,返回 float 违反类型签名
return value * 2 # 如果 value 是 str,"hello" * 2 = "hellohello",类型是 str
# 如果 value 是 int,3 * 2 = 6,类型是 int
# 看似可以,但如果 value 是 list[int],[1] * 2 = [1, 1],类型是 list[int]
# 所以实际上对于 int 和 str,返回类型确实一致
# 更安全的设计:明确输入和输出
from typing import TypeVar
T = TypeVar("T")
def prepend(items: list[T], item: T) -> list[T]:
"""在列表头部添加元素,元素类型与列表一致"""
return [item] + items
reveal_type(prepend([1, 2, 3], 0)) # list[int]
reveal_type(prepend(["a", "b"], "c")) # list[str]
形态二:约束 TypeVar------限定类型范围
python
from typing import TypeVar
# T 只能是 str 或 bytes
StrOrBytes = TypeVar("StrOrBytes", str, bytes)
def concat(first: StrOrBytes, second: StrOrBytes) -> StrOrBytes:
"""拼接两个同类型值"""
return first + second # str + str → str, bytes + bytes → bytes
reveal_type(concat("hello", " world")) # str
reveal_type(concat(b"hello", b" world")) # bytes
# ❌ concat("hello", b" world") 报错------类型变量必须一致
# StrOrBytes 在一次调用中必须绑定同一个类型
形态三:上界 TypeVar------必须是某个类型的子类
python
from typing import TypeVar
# T 必须是 Shape 的子类
class Shape:
def area(self) -> float:
return 0.0
class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
TShape = TypeVar("TShape", bound=Shape)
def total_area(shapes: list[TShape]) -> float:
"""计算形状列表的总面积"""
return sum(s.area() for s in shapes)
# OK------Circle 和 Rectangle 都是 Shape 的子类
circles: list[Circle] = [Circle(1), Circle(2)]
rects: list[Rectangle] = [Rectangle(3, 4)]
print(total_area(circles)) # 9.42...
print(total_area(rects)) # 12.0
约束 vs 上界的区别:
| 特性 | 约束 TypeVar("T", A, B) |
上界 TypeVar("T", bound=A) |
|---|---|---|
| 类型范围 | 只能是 A 或 B | A 及其所有子类 |
| 跨类型调用 | 同一次调用中 T 必须一致 | 同一次调用中 T 必须一致 |
| 可用方法 | 只能用 A 和 B 的交集方法 | 可以用 A 的所有方法 |
| 适用场景 | 有限枚举(str/bytes) | 类型层次(Shape 子类) |
泛型类:类型参数化的容器
list[T]、dict[K, V] 都是泛型类的实例。自定义泛型类同样使用 Generic:
python
from typing import Generic, TypeVar
K = TypeVar("K")
V = TypeVar("V")
class BiMap(Generic[K, V]):
"""双向映射:键→值 和 值→键 双向查找"""
def __init__(self) -> None:
self._forward: dict[K, V] = {}
self._reverse: dict[V, K] = {}
def add(self, key: K, value: V) -> None:
"""添加映射对"""
if key in self._forward:
raise KeyError(f"Key {key!r} already exists")
if value in self._reverse:
raise KeyError(f"Value {value!r} already exists")
self._forward[key] = value
self._reverse[value] = key
def by_key(self, key: K) -> V | None:
"""通过键查找值"""
return self._forward.get(key)
def by_value(self, value: V) -> K | None:
"""通过值查找键"""
return self._reverse.get(value)
def __len__(self) -> int:
return len(self._forward)
def __repr__(self) -> str:
items = ", ".join(f"{k!r}: {v!r}" for k, v in self._forward.items())
return f"BiMap({{{items}}})"
# 使用------类型参数在创建时推断
http_codes = BiMap[str, int]()
http_codes.add("OK", 200)
http_codes.add("Not Found", 404)
http_codes.add("Internal Error", 500)
print(http_codes.by_key("OK")) # 200
print(http_codes.by_value(404)) # 'Not Found'
# 类型安全:键必须是 str,值必须是 int
# http_codes.add(200, "OK") # ❌ mypy 报错:键类型不匹配
Python 3.12+ 的新语法:PEP 695
Python 3.12 引入了更简洁的泛型语法,不再需要手动创建 TypeVar 和继承 Generic:
python
# Python 3.12+ 语法
def first[T](items: list[T]) -> T:
if not items:
raise ValueError("empty list")
return items[0]
class Stack[T]:
"""类型安全的栈"""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("pop from empty stack")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("peek at empty stack")
return self._items[-1]
class BiMap[K, V]:
"""双向映射(3.12+ 语法)"""
def __init__(self) -> None:
self._forward: dict[K, V] = {}
self._reverse: dict[V, K] = {}
def add(self, key: K, value: V) -> None:
self._forward[key] = value
self._reverse[value] = key
PEP 695 语法的优势:
- 自动创建 TypeVar :不需要
T = TypeVar("T")这一行 - 作用域隔离:类型参数只在函数/类内部可见,不会污染模块命名空间
- 更简洁:减少样板代码
Protocol:结构化子类型
从「继承接口」到「满足协议」
传统的面向对象使用名义子类型(Nominal Subtyping)------子类必须显式继承父类才能被视为子类型:
python
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self) -> None: ...
class Circle(Drawable): # 必须显式继承
def draw(self) -> None:
print("Drawing circle")
class Square(Drawable): # 必须显式继承
def draw(self) -> None:
print("Drawing square")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # OK
render(Square()) # OK
问题在于:如果一个类没有继承 Drawable,但确实有 draw() 方法呢?
python
class SVGElement:
"""来自第三方库,没有继承 Drawable"""
def draw(self) -> None:
print("Rendering SVG")
render(SVGElement()) # ❌ mypy 报错:SVGElement 不是 Drawable 的子类
Python 的鸭子类型哲学是"只要有这个方法就行",但名义子类型强制要求"必须显式声明"------这就是 Protocol 要解决的矛盾。
Protocol 的基本用法
python
from typing import Protocol
class Drawable(Protocol):
"""协议:只要实现了 draw() 方法,就是 Drawable"""
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class SVGElement:
"""不需要继承 Drawable,只要实现了 draw() 就满足协议"""
def draw(self) -> None:
print("Rendering SVG")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # ✅ OK
render(SVGElement()) # ✅ OK------结构兼容
Protocol 不要求继承,只要求结构匹配 ------这就是结构化子类型(Structural Subtyping)。
Protocol 的类型检查规则
python
from typing import Protocol
class Closeable(Protocol):
"""协议:有 close() 方法的对象"""
def close(self) -> None: ...
class FileHandle:
def __init__(self, path: str) -> None:
self.path = path
def close(self) -> None:
print(f"Closing {self.path}")
def read(self) -> str:
return "data"
class DatabaseConnection:
def close(self) -> None:
print("Closing database connection")
def safe_close(resource: Closeable) -> None:
resource.close()
# ✅ FileHandle 有 close() 方法,满足 Closeable 协议
safe_close(FileHandle("/tmp/test.txt"))
# ✅ DatabaseConnection 有 close() 方法,满足 Closeable 协议
safe_close(DatabaseConnection())
# ❌ 以下不满足协议
class PoorResource:
def cleanup(self) -> None: # 方法名不同
pass
# safe_close(PoorResource()) # mypy 报错:缺少 close 方法
协议属性
Protocol 不仅可以定义方法,还可以定义属性:
python
from typing import Protocol
class Named(Protocol):
"""协议:有 name 属性的对象"""
name: str
class Person:
def __init__(self, name: str) -> None:
self.name = name
class Product:
def __init__(self, name: str, price: float) -> None:
self.name = name
self.price = price
def greet(entity: Named) -> str:
return f"Hello, {entity.name}!"
print(greet(Person("Alice"))) # Hello, Alice!
print(greet(Product("Widget", 9.99))) # Hello, Widget!
可初始化协议:__init__ 约束
Protocol 可以约束构造函数签名:
python
from typing import Protocol, TypeVar, Generic
T = TypeVar("T", bound="Constructable")
class Constructable(Protocol):
"""协议:可以用一个 int 值构造的对象"""
def __init__(self, value: int) -> None: ...
def create_batch(cls: type[T], values: list[int]) -> list[T]:
"""用同一个类构造多个实例"""
return [cls(v) for v in values]
class Counter:
def __init__(self, value: int) -> None:
self.value = value
def __repr__(self) -> str:
return f"Counter({self.value})"
counters = create_batch(Counter, [1, 2, 3])
print(counters) # [Counter(1), Counter(2), Counter(3)]
泛型 + Protocol:类型安全的最高境界
场景:可迭代容器协议
将泛型和 Protocol 组合,可以描述更复杂的协议------比如"一个可迭代的、有长度的容器":
python
from typing import Protocol, TypeVar, Iterator, overload
T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
class SizedIterable(Protocol[T_co]):
"""协议:有长度且可迭代的容器"""
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[T_co]: ...
def report(container: SizedIterable[object]) -> str:
"""报告容器信息------接受任何有长度且可迭代的对象"""
return f"Container with {len(container)} items: {list(container)}"
# list[int]、tuple[str, ...]、set[bool] 都满足 SizedIterable[object]
print(report([1, 2, 3])) # Container with 3 items: [1, 2, 3]
print(report(("a", "b"))) # Container with 2 items: ['a', 'b']
print(report({True, False})) # Container with 2 items: [True, False]
协变与逆变
泛型类型参数有三种变体(Variance),决定了子类型关系是否传递:
类型参数变体
协变 Covariant
子类型关系保持方向
逆变 Contravariant
子类型关系反转
不变 Invariant
无子类型关系
Producer
只产出 T
如 Iterator[T]
Consumer
只消费 T
如 Callable[[T], None]
Producer + Consumer
既产出又消费 T
如 list[T]
list[int] 是 list[object]
的子类型?❌ list 不变
Iterator[int] 是
Iterator[object] 的子类型?✅ 协变
协变(Covariant) :如果 Dog 是 Animal 的子类型,则 Container[Dog] 是 Container[Animal] 的子类型。适用于只产出 T 的容器 (如 Iterator[T])。
python
from typing import TypeVar, Protocol, Iterator
T_co = TypeVar("T_co", covariant=True)
class Producer(Protocol[T_co]):
"""协变协议:只产出 T_co"""
def produce(self) -> T_co: ...
def consume_all(producer: Producer[object]) -> None:
"""消费任意类型的生产者"""
item = producer.produce()
print(f"Got: {item}")
class StringProducer:
def produce(self) -> str:
return "hello"
# ✅ StringProducer 满足 Producer[str]
# ✅ Producer[str] 是 Producer[object] 的子类型(协变)
consume_all(StringProducer()) # OK
逆变(Contravariant) :如果 Dog 是 Animal 的子类型,则 Consumer[Animal] 是 Consumer[Dog] 的子类型。适用于只消费 T 的容器。
python
from typing import TypeVar, Protocol
T_contra = TypeVar("T_contra", contravariant=True)
class Handler(Protocol[T_contra]):
"""逆变协议:只消费 T_contra"""
def handle(self, value: T_contra) -> None: ...
def use_handler(handler: Handler[str]) -> None:
"""使用字符串处理器"""
handler.handle("hello")
class ObjectPrinter:
def handle(self, value: object) -> None:
print(f"Printing: {value}")
# ✅ ObjectPrinter 满足 Handler[object]
# ✅ Handler[object] 是 Handler[str] 的子类型(逆变------方向反转)
use_handler(ObjectPrinter()) # OK
不变(Invariant) :既产出又消费 T 的容器。list[T] 就是不变的------因为 list[T] 既可以通过 __getitem__ 产出 T,也可以通过 append 消费 T。
python
# list 是不变的
animals: list[object] = []
dogs: list[str] = ["rex", "buddy"]
# ❌ 以下赋值不合法
# animals = dogs # mypy 报错
# 原因:如果允许,就可以 animals.append(42)------向 list[str] 中插入 int!
变体选择规则
| 操作 | 变体 | TypeVar 声明 |
|---|---|---|
| 只产出 T(返回值、迭代) | 协变 | TypeVar("T", covariant=True) |
| 只消费 T(参数、写入) | 逆变 | TypeVar("T", contravariant=True) |
| 既产出又消费 T | 不变 | TypeVar("T")(默认) |
原则 :如果不确定该用什么变体,就用不变(默认)。协变和逆变只在定义 Protocol 或抽象基类时需要考虑,普通泛型类不需要。
工程实战:插件系统的类型安全架构
将泛型和 Protocol 组合在一个真实的插件系统中:
python
"""插件系统:演示泛型 + Protocol 的工程化组合"""
from typing import Protocol, TypeVar, Generic
from dataclasses import dataclass
# ========== 第一层:定义插件协议 ==========
class Plugin(Protocol):
"""所有插件必须满足的协议"""
name: str
version: str
def initialize(self) -> None: ...
def shutdown(self) -> None: ...
class ConfigPlugin(Protocol):
"""配置插件的扩展协议"""
def get_config(self, key: str) -> str | None: ...
def set_config(self, key: str, value: str) -> None: ...
class MetricsPlugin(Protocol):
"""指标插件的扩展协议"""
def record(self, metric: str, value: float) -> None: ...
def query(self, metric: str) -> list[float]: ...
# ========== 第二层:泛型插件注册器 ==========
P = TypeVar("P", bound=Plugin)
class PluginRegistry(Generic[P]):
"""类型安全的插件注册器------只接受满足 P 协议的插件"""
def __init__(self) -> None:
self._plugins: dict[str, P] = {}
def register(self, plugin: P) -> None:
"""注册插件"""
if plugin.name in self._plugins:
raise ValueError(f"Plugin {plugin.name!r} already registered")
self._plugins[plugin.name] = plugin
plugin.initialize()
def get(self, name: str) -> P | None:
"""按名称获取插件"""
return self._plugins.get(name)
def all_plugins(self) -> list[P]:
"""获取所有已注册插件"""
return list(self._plugins.values())
def shutdown_all(self) -> None:
"""关闭所有插件"""
for plugin in self._plugins.values():
plugin.shutdown()
self._plugins.clear()
# ========== 第三层:具体插件实现 ==========
@dataclass
class FileConfigPlugin:
"""文件配置插件------满足 ConfigPlugin 和 Plugin 协议"""
name: str
version: str
_config: dict[str, str] | None = None
def initialize(self) -> None:
self._config = {"host": "localhost", "port": "3306"}
print(f"[{self.name}] Initialized with config")
def shutdown(self) -> None:
self._config = None
print(f"[{self.name}] Shutdown")
def get_config(self, key: str) -> str | None:
if self._config is None:
return None
return self._config.get(key)
def set_config(self, key: str, value: str) -> None:
if self._config is not None:
self._config[key] = value
@dataclass
class PrometheusMetricsPlugin:
"""Prometheus 指标插件------满足 MetricsPlugin 和 Plugin 协议"""
name: str
version: str
_metrics: dict[str, list[float]] | None = None
def initialize(self) -> None:
self._metrics = {}
print(f"[{self.name}] Connected to Prometheus")
def shutdown(self) -> None:
self._metrics = None
print(f"[{self.name}] Disconnected from Prometheus")
def record(self, metric: str, value: float) -> None:
if self._metrics is None:
return
self._metrics.setdefault(metric, []).append(value)
def query(self, metric: str) -> list[float]:
if self._metrics is None:
return []
return self._metrics.get(metric, [])
# ========== 第四层:使用泛型注册器 ==========
# 创建基础注册器------接受所有 Plugin
base_registry = PluginRegistry[Plugin]()
# 创建配置插件注册器------只接受 ConfigPlugin
config_registry = PluginRegistry[ConfigPlugin]()
# 创建指标插件注册器------只接受 MetricsPlugin
metrics_registry = PluginRegistry[MetricsPlugin]()
# 注册插件
file_config = FileConfigPlugin(name="file-config", version="1.0.0")
prom_metrics = PrometheusMetricsPlugin(name="prometheus", version="2.0.0")
# ✅ FileConfigPlugin 满足 ConfigPlugin 协议(有 get_config/set_config)
config_registry.register(file_config)
print(config_registry.get("file-config").get_config("host")) # localhost
# ✅ PrometheusMetricsPlugin 满足 MetricsPlugin 协议(有 record/query)
metrics_registry.register(prom_metrics)
metrics_registry.get("prometheus").record("cpu_usage", 0.75)
metrics_registry.get("prometheus").record("cpu_usage", 0.82)
print(metrics_registry.get("prometheus").query("cpu_usage")) # [0.75, 0.82]
# ✅ 两个插件也满足基础 Plugin 协议
base_registry.register(file_config)
base_registry.register(prom_metrics)
# 清理
config_registry.shutdown_all()
metrics_registry.shutdown_all()
base_registry.shutdown_all()
Protocol vs ABC:何时用哪个
核心区别
| 维度 | Protocol(结构子类型) | ABC(名义子类型) |
|---|---|---|
| 类型关系 | 隐式,满足结构即可 | 显式,必须继承 |
| 第三方库 | 无需修改代码即可适配 | 必须子类化或注册 |
| 运行时行为 | 可选 @runtime_checkable |
必须使用 isinstance |
| 方法检查 | 静态检查时验证 | 运行时验证 |
| 多继承 | 天然支持(结构匹配) | 需要 MRO 处理 |
| 默认实现 | 不支持 | 支持(@abstractmethod + 具体方法) |
什么时候用 Protocol
- 与第三方库交互------不想或不能修改第三方类的继承关系
- 鸭子类型需要静态检查------"有这个方法就行"的语义
- 协议定义与实现分离------定义者不关心实现者
- 多态不需要继承链------多个无关类共享行为
什么时候用 ABC
- 需要默认实现------基类提供通用方法,子类只需覆盖关键方法
- 需要运行时
isinstance检查 ------Protocol 的@runtime_checkable只检查方法存在性,不检查签名 - 语义要求"是一个"关系 ------
Circle是一个Shape,而不只是"有area()方法"
@runtime_checkable 的陷阱
python
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
class FakeCloseable:
def close(self, force: bool = False) -> None: # 签名不同
pass
# isinstance 检查通过------因为只检查方法是否存在
print(isinstance(FakeCloseable(), Closeable)) # True!
# 但 mypy 静态检查会报错------签名不匹配
# def use(c: Closeable) -> None:
# c.close() # mypy 知道 close 不接受参数
原则 :
@runtime_checkable只应用于"方法签名简单、无重载"的协议。复杂协议应依赖静态检查。
高级模式:递归类型与自引用泛型
递归 Protocol:树形结构
python
from typing import Protocol, Iterator
class TreeNode(Protocol):
"""协议:树节点------子节点也必须满足 TreeNode"""
value: int
children: list["TreeNode"]
def sum_tree(node: TreeNode) -> int:
"""递归计算树节点值之和"""
total = node.value
for child in node.children:
total += sum_tree(child)
return total
class SimpleNode:
"""满足 TreeNode 协议的具体类"""
def __init__(self, value: int, children: list[SimpleNode] | None = None) -> None:
self.value = value
self.children = children or []
# 构建树
root = SimpleNode(1, [
SimpleNode(2, [SimpleNode(4), SimpleNode(5)]),
SimpleNode(3),
])
print(sum_tree(root)) # 1 + 2 + 4 + 5 + 3 = 15
自引用泛型:流畅接口模式
python
from typing import TypeVar, Generic
T = TypeVar("T", bound="Builder")
class Builder(Generic[T]):
"""泛型构建器------子类方法返回自身类型,支持链式调用"""
def _self(self) -> T:
return self # type: ignore[return-value]
def set_name(self, name: str) -> T:
self.name = name # type: ignore[attr-defined]
return self._self()
def set_age(self, age: int) -> T:
self.age = age # type: ignore[attr-defined]
return self._self()
def build(self) -> dict[str, object]:
return {"name": getattr(self, "name", ""), "age": getattr(self, "age", 0)}
class PersonBuilder(Builder["PersonBuilder"]):
"""具体的构建器------链式调用返回 PersonBuilder 而非 Builder"""
def set_email(self, email: str) -> "PersonBuilder":
self.email = email
return self
def build(self) -> dict[str, object]:
result = super().build()
result["email"] = getattr(self, "email", "")
return result
# 链式调用------类型安全
person = (PersonBuilder()
.set_name("Alice")
.set_age(30)
.set_email("alice@example.com") # 只在 PersonBuilder 上可用
.build())
print(person) # {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}
Protocol 与 @overload 的配合
当 Protocol 方法需要根据参数类型返回不同类型时,结合 @overload:
python
from typing import Protocol, overload, Literal
class ConfigSource(Protocol):
"""配置源协议------根据类型参数返回不同类型的值"""
@overload
def get(self, key: str, type_hint: Literal["str"]) -> str: ...
@overload
def get(self, key: str, type_hint: Literal["int"]) -> int: ...
@overload
def get(self, key: str, type_hint: Literal["float"]) -> float: ...
@overload
def get(self, key: str, type_hint: str) -> str | int | float: ...
def get(self, key: str, type_hint: str = "str") -> str | int | float: ...
class EnvConfig:
"""环境变量配置源------满足 ConfigSource 协议"""
def __init__(self) -> None:
self._data: dict[str, str] = {
"HOST": "localhost",
"PORT": "3306",
"TIMEOUT": "30.5",
}
@overload
def get(self, key: str, type_hint: Literal["str"]) -> str: ...
@overload
def get(self, key: str, type_hint: Literal["int"]) -> int: ...
@overload
def get(self, key: str, type_hint: Literal["float"]) -> float: ...
@overload
def get(self, key: str, type_hint: str) -> str | int | float: ...
def get(self, key: str, type_hint: str = "str") -> str | int | float:
value = self._data.get(key, "")
if type_hint == "int":
return int(value)
if type_hint == "float":
return float(value)
return value
def use_config(source: ConfigSource) -> None:
host = source.get("HOST", "str") # mypy 推断为 str
port = source.get("PORT", "int") # mypy 推断为 int
timeout = source.get("TIMEOUT", "float") # mypy 推断为 float
print(f"{host}:{port} (timeout={timeout}s)")
use_config(EnvConfig()) # localhost:3306 (timeout=30.5s)
常见陷阱与避坑
陷阱一:Protocol 的方法必须是抽象的
python
from typing import Protocol
class BadProtocol(Protocol):
def greet(self) -> str:
return "hello" # ❌ Protocol 方法不应该有实现
# mypy 会警告:Protocol methods should not have implementations
class GoodProtocol(Protocol):
def greet(self) -> str: ... # ✅ 用 ... 表示抽象
陷阱二:Protocol 不能用作基类
python
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
class MyResource(Closeable): # ⚠️ 不推荐------虽然语法合法,但违反 Protocol 的设计意图
def close(self) -> None:
print("closing")
# 正确做法:让 MyResource 独立实现,不继承 Protocol
class MyResource2:
def close(self) -> None:
print("closing")
# mypy 会自动检测 MyResource2 满足 Closeable 协议
def safe_close(c: Closeable) -> None:
c.close()
safe_close(MyResource2()) # ✅ OK
例外 :如果确实需要在 Protocol 中提供默认实现(Python 3.12+),可以使用
@non_protocol装饰器标注非协议方法,但这超出了 Protocol 的典型用法。
陷阱三:检查 Protocol 兼容性时忽略属性
python
from typing import Protocol
class Named(Protocol):
name: str
def greet(self) -> str: ...
class Person:
def __init__(self, name: str) -> None:
self._name = name # 注意:属性名是 _name
@property
def name(self) -> str: # ✅ 通过 property 提供 name
return self._name
def greet(self) -> str:
return f"Hello, {self.name}"
def say_hello(entity: Named) -> str:
return entity.greet()
say_hello(Person("Alice")) # ✅ OK------property 满足属性协议
Protocol 检查属性时,property 和实例属性都满足要求。
陷阱四:泛型协变误用
python
from typing import TypeVar, Generic
T_co = TypeVar("T_co", covariant=True)
class Box(Generic[T_co]):
"""协变容器------只产出 T"""
def __init__(self, value: T_co) -> None:
self._value = value
def get(self) -> T_co:
return self._value
# ❌ 协变容器不能有消费 T 的方法
# def set(self, value: T_co) -> None:
# self._value = value
# mypy 报错:Cannot use a covariant type variable as a parameter
选型决策树:名义子类型 vs 结构子类型
是
否
多个无关类
共享行为
同一继承链
共享行为
可控,且语义是
is-a 关系
不可控,或需要
适配第三方类
是
否
是
否
需要定义一个接口
是否需要
默认实现?
使用 ABC
抽象基类提供通用方法
实现者是否可控?
使用 Protocol
结构匹配,无需继承
使用 ABC
显式继承表达 is-a
是否需要
运行时检查?
ABC + isinstance
完整的类型验证
ABC 纯静态
与 Protocol 效果类似
是否需要
运行时检查?
@runtime_checkable
注意:只检查方法存在性
纯静态 Protocol
推荐:最安全
最佳实践总结
泛型最佳实践
- 类型变量命名 :用简短的大写名称(
T、K、V),上界用有语义的名称(TShape) - 优先使用 PEP 695 语法(Python 3.12+):减少样板代码
- 默认使用不变 TypeVar:只在 Protocol 定义中考虑协变/逆变
- 约束优先于上界 :如果类型只能是有限几个,用
TypeVar("T", A, B)而不是bound=Union[A, B] - 泛型类不要过度参数化:超过 3 个类型参数时,考虑拆分类
Protocol 最佳实践
- Protocol 方法用
...而非pass:...明确表示"抽象,无实现" - 不要让实现类继承 Protocol:让 mypy 通过结构匹配自动检测
- 避免
@runtime_checkable:除非确实需要isinstance检查,且方法签名简单 - Protocol 应该小而专注 :一个 Protocol 描述一个能力(如
Closeable、Iterable),而不是多个(如CloseableAndIterableAndNamed) - Protocol 放在类型定义模块:与实现类分离,方便跨模块复用
泛型 + Protocol 的组合模式
python
from typing import Protocol, TypeVar
# 模式一:泛型函数 + Protocol 约束
T = TypeVar("T")
class Comparable(Protocol):
def __lt__(self, other: "Comparable") -> bool: ...
def min_value(a: T, b: T) -> T:
"""仅适用于满足 Comparable 协议的类型"""
return a if a < b else b # 需要在调用点确保 T 满足 Comparable
# 模式二:泛型类 + Protocol 边界
TComparable = TypeVar("TComparable", bound=Comparable)
class SortedContainer(Generic[TComparable]):
"""只接受可比较的元素"""
def __init__(self) -> None:
self._items: list[TComparable] = []
def add(self, item: TComparable) -> None:
import bisect
bisect.insort(self._items, item)
def items(self) -> list[TComparable]:
return self._items.copy()
小结
| 概念 | 一句话定位 | 核心价值 | 核心风险 |
|---|---|---|---|
| 泛型 | 类型参数化,一次编写多种类型复用 | 类型安全的通用代码 | 过度参数化增加复杂度 |
| TypeVar | 泛型的类型变量 | 输入输出类型关联 | 变体选择不当导致类型不安全 |
| Protocol | 结构化子类型,不继承也是子类型 | 鸭子类型的静态检查 | @runtime_checkable 只检查存在性 |
| 协变/逆变 | 泛型类型参数的子类型传递方向 | 灵活的类型兼容性 | 误用导致运行时类型不安全 |
泛型和 Protocol 是 Python 类型系统的两个高阶武器------泛型解决"类型安全的复用"问题,Protocol 解决"不继承也是子类型"问题。两者组合,可以在保持 Python 灵活性的同时,获得接近静态语言的类型安全保障。
在 属性查找顺序:实例 → 类 → 父类的完整 MRO 中曾讨论过 Python 的属性查找机制------Protocol 的结构匹配正是建立在 __getattribute__ 的属性查找链之上。理解了底层机制,再回头看 Protocol 的类型检查,就会发现一切都是"属性存在性 + 签名匹配"的自然推演。
如果这篇文章对理解 Python 泛型与 Protocol 有帮助,点赞收藏让更多人看到!关注专栏,持续获取 Python 进阶干货。