泛型与 Protocol:结构化子类型的地道写法

文章目录

真正的类型安全不是"每个类都继承一个接口",而是"只要长得像鸭子,就是鸭子"------Protocol 让鸭子类型获得了静态检查的能力。


从类型注解的局限说起

类型注解进阶:UnionOptionalAnyCallable 中,梳理了 UnionOptionalAnyCallable 四个核心类型构造器。但留了两个关键问题没有解答:

  1. 如何写出类型安全的通用函数? ------list[int]list[str] 共享相同的结构,但 list 本身不够精确,需要一种方式表达"元素类型可变,但行为固定"
  2. 如何描述"有这个方法就行"的接口? ------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) :如果 DogAnimal 的子类型,则 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) :如果 DogAnimal 的子类型,则 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

  1. 与第三方库交互------不想或不能修改第三方类的继承关系
  2. 鸭子类型需要静态检查------"有这个方法就行"的语义
  3. 协议定义与实现分离------定义者不关心实现者
  4. 多态不需要继承链------多个无关类共享行为

什么时候用 ABC

  1. 需要默认实现------基类提供通用方法,子类只需覆盖关键方法
  2. 需要运行时 isinstance 检查 ------Protocol 的 @runtime_checkable 只检查方法存在性,不检查签名
  3. 语义要求"是一个"关系 ------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

推荐:最安全


最佳实践总结

泛型最佳实践

  1. 类型变量命名 :用简短的大写名称(TKV),上界用有语义的名称(TShape
  2. 优先使用 PEP 695 语法(Python 3.12+):减少样板代码
  3. 默认使用不变 TypeVar:只在 Protocol 定义中考虑协变/逆变
  4. 约束优先于上界 :如果类型只能是有限几个,用 TypeVar("T", A, B) 而不是 bound=Union[A, B]
  5. 泛型类不要过度参数化:超过 3 个类型参数时,考虑拆分类

Protocol 最佳实践

  1. Protocol 方法用 ... 而非 pass... 明确表示"抽象,无实现"
  2. 不要让实现类继承 Protocol:让 mypy 通过结构匹配自动检测
  3. 避免 @runtime_checkable :除非确实需要 isinstance 检查,且方法签名简单
  4. Protocol 应该小而专注 :一个 Protocol 描述一个能力(如 CloseableIterable),而不是多个(如 CloseableAndIterableAndNamed
  5. 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 进阶干货。

相关推荐
独隅7 小时前
PyCharm 接入 Codex 的全面指南
ide·python·pycharm
OrangeForce7 小时前
Monknow 书签导出工具:从本地存储提取数据并转为标准 HTML 书签
javascript·chrome·python·edge·html·firefox
才兄说7 小时前
机器人二次开发机器狗巡检?实时路径动态更新
python
yaoxin5211238 小时前
414. Java 文件操作基础 - 批量压缩与索引:将154首十四行诗高效存储为带目录的二进制文件
java·windows·python
沐知全栈开发8 小时前
Servlet 表单数据处理指南
开发语言
超梦dasgg8 小时前
详细讲解:WebMvcConfigurer 接口
java·开发语言·spring
繁星星繁8 小时前
Python基础语法(二)
android·服务器·python
毋语天8 小时前
Pandas 数据处理进阶:缺失值、合并、分组聚合与透视表
python·数据分析·pandas·数据清洗·透视表
阿里嘎多学长8 小时前
2026-05-22 GitHub 热点项目精选
开发语言·程序员·github·代码托管