Python typing.Protocol的结构子类型化实践
在Python中,传统继承(如class MyList(list))是一种常见的代码复用方式,但它也创建了强耦合。typing.Protocol引入了结构子类型化,允许你定义对象必须具备的“形状”(方法与属性),而无需继承。只要一个对象拥有协议声明的方法和属性,它就“满足”该协议。本指南将手把手教你如何使用Protocol编写更灵活、解耦且类型安全的代码。
1. 理解核心概念:什么是结构子类型化?
明确目标:在不依赖继承的情况下,让函数接受“任何拥有特定方法的对象”。
理解问题:假设你需要一个函数来“打印对象的描述”。如果让所有可打印的类都继承一个Printable基类,会导致类与框架强耦合,增加维护成本。
解决方案:定义一个Printable协议,声明一个describe()方法。任何拥有describe()方法的类,无论其继承关系如何,都能被该函数接受。
from typing import Protocol, runtime_checkable
# 定义一个协议
@runtime_checkable
class Printable(Protocol):
def describe(self) -> str:
...
# 一个不相关继承体系的类
class Book:
def __init__(self, title: str):
self.title = title
def describe(self) -> str: # 实现了describe方法
return f"《{self.title}》"
# 另一个完全不同的类
class Animal:
def __init__(self, name: str):
self.name = name
def describe(self) -> str: # 也实现了describe方法
return f"Animal: {self.name}"
# 接受任何“可打印”对象的函数
def print_description(obj: Printable) -> None: # 参数类型标注为协议
print(obj.describe())
# 两个类都能正常工作,尽管它们没有共同的父类
book = Book("Python实践")
animal = Animal("Dog")
print_description(book) # 输出: 《Python实践》
print_description(animal) # 输出: Animal: Dog
核心要点:print_description函数只关心传入的对象是否有一个describe()方法,而不关心它的类名或继承树。这就是结构子类型化。
2. 定义你的第一个协议
掌握语法:使用typing.Protocol作为基类来定义协议。
编写协议类:
- 导入模块:
from typing import Protocol, runtime_checkable - 定义类:创建一个类,继承自
Protocol。 - 声明成员:使用类型注解声明协议要求的方法和属性。方法体可以使用省略号
...,或包含简要文档字符串,但通常不包含具体实现。 - 应用装饰器(可选但推荐):添加
@runtime_checkable装饰器,这允许你在运行时使用isinstance()检查对象是否符合协议。
from typing import Protocol, runtime_checkable
import abc
@runtime_checkable
class SupportsClose(Protocol):
"""
任何拥有 `.close()` 方法的对象都符合此协议。
"""
def close(self) -> None:
...
@runtime_checkable
class Sized(Protocol):
"""
任何拥有只读属性 `size` 的对象都符合此协议。
"""
@property
def size(self) -> int:
...
@runtime_checkable
class NamedObject(Protocol):
"""
要求同时具备 `name` 属性和 `get_id` 方法的复杂协议。
"""
name: str # 声明一个属性
def get_id(self) -> int:
...
3. 使用协议进行类型标注
在函数签名中使用:将协议类型作为函数参数或返回值的注解。这是协议最核心的用途。
编写依赖函数:
from typing import Protocol
class Serializer(Protocol):
def serialize(self, data: dict) -> str:
...
def save_to_file(data: dict, serializer: Serializer, filename: str) -> None:
"""使用任何符合Serializer协议的对象来保存数据。"""
serialized_data = serializer.serialize(data) # 调用协议方法
with open(filename, 'w') as f:
f.write(serialized_data)
实现协议以供调用:
import json
class JsonSerializer:
"""一个普通的类,它“实现”了Serializer协议。"""
def serialize(self, data: dict) -> str: # 方法签名匹配协议
return json.dumps(data)
class XmlSerializer:
def serialize(self, data: dict) -> str: # 另一个实现
return f"<data>{data}</data>" # 示例性转换
# 调用时,两种序列化器都能工作
data = {"key": "value"}
save_to_file(data, JsonSerializer(), "data.json")
save_to_file(data, XmlSerializer(), "data.xml")
4. 实现协议的三种常见模式
选择适合的模式:
| 模式 | 描述 | 适用场景 |
|---|---|---|
| 鸭子类型式实现 | 不声明继承,仅实现相同的方法签名。这是最常见、最Pythonic的方式。 | 大多数第三方库的类、独立定义的类。 |
| 显式继承协议 | 在类定义中明确声明继承自协议。这提供了清晰的文档意图,并确保静态类型检查器更易识别。 | 你控制代码且希望明确契约关系。 |
| 混合继承 | 同时继承协议和其他基类。 | 需要组合多个协议或复用已有逻辑。 |
模式一:鸭子类型式实现(最常用)
如上文JsonSerializer所示,无需任何特殊语法,只要方法匹配即可。
模式二:显式继承协议
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None: ...
# 显式声明:FileLogger 承诺实现 Logger 协议
class FileLogger(Logger): # 继承协议
def __init__(self, filepath: str):
self.filepath = filepath
def log(self, message: str) -> None: # 必须实现协议方法
with open(self.filepath, 'a') as f:
f.write(message + '\n')
模式三:混合继承
from typing import Protocol
class Printable(Protocol):
def to_string(self) -> str: ...
class Serializable(Protocol):
def to_json(self) -> str: ...
class BaseModel:
"""一个提供基础功能的普通基类。"""
def validate(self) -> bool:
return True
# 同时满足打印、序列化要求,并继承基础模型功能
class Report(BaseModel, Printable, Serializable):
def __init__(self, data: dict):
self.data = data
def to_string(self) -> str: # 实现 Printable
return str(self.data)
def to_json(self) -> str: # 实现 Serializable
import json
return json.dumps(self.data)
# 使用示例
def print_and_save(item: Printable & Serializable) -> None: # 注:Python语法不支持`&`,此处仅为说明意图
print(item.to_string())
# save(item.to_json()) ...
注意:Python的类型系统使用
Union或交叉类型(typing.Intersection,目前仍在开发中)来处理多协议约束。在实际函数注解中,你可能需要使用Union[Printable, Serializable](表示满足任一即可)或编写一个包含两者所有方法的更大协议。
5. 验证对象是否符合协议
启用运行时检查:确保在定义协议时使用了@runtime_checkable装饰器。
使用isinstance()进行检查:
from typing import Protocol, runtime_checkable
import sys
@runtime_checkable
class Sized(Protocol):
@property
def size(self) -> int: ...
@runtime_checkable
class Hashable(Protocol):
def __hash__(self) -> int: ...
# 测试对象
my_list = [1, 2, 3]
my_dict = {"a": 1}
print(isinstance(my_list, Sized)) # True,列表有 `__len__`,而Python认为`size`可由`__len__`满足?注意:协议检查的是属性/方法是否存在,而不是名称匹配。`Sized`协议要求`size`属性,而列表没有该属性,所以这实际上是False。
# 修正:让我们定义一个更精确的协议
@runtime_checkable
class HasLength(Protocol):
def __len__(self) -> int: ...
print(isinstance(my_list, HasLength)) # True
print(isinstance(my_dict, HasLength)) # True
print(isinstance("hello", HasLength)) # True
# 检查一个自定义类
class Connection:
def close(self):
pass
class FakeLogger:
def log(self, msg: str):
pass
# 定义一个协议用于检查
@runtime_checkable
class Closable(Protocol):
def close(self) -> None: ...
print(isinstance(Connection(), Closable)) # True
print(isinstance(FakeLogger(), Closable)) # False
重要提示:运行时isinstance检查只验证方法/属性的存在性,不验证其类型签名或行为。静态类型检查器(如mypy)会进行更严格的类型签名检查。
6. 高级用法与最佳实践
定义复杂协议:协议可以包含属性、类方法、静态方法,甚至使用泛型。
from typing import Protocol, TypeVar, List, Iterable
T = TypeVar('T')
class Stack(Protocol[T]):
"""一个泛型栈协议。"""
def push(self, item: T) -> None: ...
def pop(self) -> T: ...
def is_empty(self) -> bool: ...
# 一个具体的实现
class SimpleStack:
def __init__(self):
self._items: list = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if self.is_empty():
raise IndexError("pop from empty stack")
return self._items.pop()
def is_empty(self) -> bool:
return len(self._items) == 0
def process_stack(stack: Stack[int]) -> None:
"""处理一个存储整数的栈。"""
stack.push(42)
print(stack.pop()) # 42
# 检查类型兼容性
s: Stack[int] = SimpleStack() # 类型检查器认为兼容
process_stack(s)
最佳实践:
- 协议命名:通常使用能描述能力的名称,如
SupportsClose、Drawable、Serializable,而不是Closeable、Draw。 - 定义最小化:协议应专注于一个特定的能力,遵循接口隔离原则(ISP)。
- 组合优于继承:可以通过继承协议来组合它们。
- 为第三方类型创建协议:为没有类型提示的第三方库对象定义协议,以提供类型安全。
- 结合
@abstractmethod:如果你想在协议中提供一些默认实现(尽管不常见),可以混合使用。但通常协议应保持纯粹。
from typing import Protocol, runtime_checkable
from abc import abstractmethod
@runtime_checkable
class AbstractRepository(Protocol):
"""结合了协议和抽象方法。"""
@abstractmethod
def get(self, id: int) -> dict: ...
@abstractmethod
def save(self, entity: dict) -> None: ...
协变与逆变:当协议包含泛型时,需要注意类型的协变(covariant)与逆变(contravariant)规则。对于只读数据(如生产者),使用协变TypeVar('T_co', covariant=True);对于只接受数据(如消费者),使用逆变TypeVar('T_contra', contravariant=True);对于读写都有的,使用不变TypeVar('T')。
性能考量:@runtime_checkable装饰器会带来微小的性能开销,因为它需要通过反射检查对象。如果只在静态分析时使用协议,可以省略此装饰器。
与抽象基类(ABC)的选择:
- 使用
Protocol:当你需要定义“形状”约束,且实现类可能来自不同继承树时。它是鸭子类型的正式表达。 - 使用
ABC:当你想提供一些默认实现、强制继承关系,或使用isinstance进行可靠的运行时类型检查时(因为ABC的__subclasshook__是精确的,而Protocol的检查可能较宽松)。

暂无评论,快来抢沙发吧!