Python get_protocol_members 怎么读取 Protocol 成员集合
来源:17golang原创
时间:2026-10-06 13:14:21 487浏览 收藏
在 Python 3.13 及以上版本中,读取 Protocol 的成员集合,直接调用 typing.get_protocol_members(协议类) 即可。它返回 frozenset[str],内容是协议要求的方法名和属性名;如果传入的对象不是协议类,则会抛出 TypeError。
官方文档:https://docs.python.org/3/library/typing.html#typing.get_protocol_members
这个 API 看似只是一个小型反射工具,却能解决插件注册、协议文档生成和接口清单检查中的一个常见故障:手工读取 __dict__ 时,当前类里看得到的名称不等于完整的协议契约。
一、成员探测故障影响了哪些结果
设想一个插件检查器要把协议要求输出成日志。第一版实现遍历 protocol.__dict__,开发环境里的简单协议看起来正常;协议一旦继承另一个协议,生成的清单就漏掉父协议成员。与此同时,__module__、__doc__ 等类内部名称又混进结果,最终让“缺少哪些接口”的提示失去可信度。
from typing import Protocol
class Closable(Protocol):
def close(self) -> None: ...
class Repository(Closable, Protocol):
name: str
def get(self, key: str) -> bytes | None: ...
# 只看当前类命名空间会漏掉继承来的 close,且还要自己过滤内部名称。
guessed = {
name for name in Repository.__dict__
if not name.startswith("_")
}
print(guessed)
这里的影响不在类型检查器本身,而在运行时工具:它可能生成不完整的文档、注册错误的适配器,或向用户报告一个并不存在的“接口完整”状态。
二、按复现时间线锁定触发条件
把故障压缩成最小案例后,触发过程很清楚:
- 先定义只包含方法的基础协议
Closable。 - 让
Repository继承它,并新增注解属性name和方法get。 - 读取
Repository.__dict__,结果只能代表当前类命名空间。 - 当工具把这个结果当成完整契约时,继承成员
close被漏掉。
注解属性又是另一个容易误判的点。name: str 主要记录在注解信息里,不应要求它像普通赋值属性那样出现在类字典的同一位置。自己拼接 __dict__ 与 __annotations__,还要持续跟进 typing 内部排除名单,维护成本会不断上升。
三、根因是把类命名空间当成协议契约
Protocol 描述的是结构化类型契约。根据 PEP 544,协议成员可以是方法,也可以是变量;一个具体类型是否满足协议,还要由静态类型检查器判断成员类型和方法签名是否兼容。类的 __dict__ 只是某一层命名空间,不承担“给出完整协议成员集合”的公开承诺。

因此,故障并不是“过滤条件少写了一个名称”,而是选择了错误的数据入口。继续扩充私有过滤列表,只会把偶发错误变成长期兼容负担。
四、改用 get_protocol_members 完成修复
Python 3.13 为这个任务加入了公开函数 typing.get_protocol_members。修复后的代码只需要把协议类传进去:
from typing import Protocol, get_protocol_members
class Closable(Protocol):
def close(self) -> None: ...
class Repository(Closable, Protocol):
name: str
def get(self, key: str) -> bytes | None: ...
members = get_protocol_members(Repository)
print(members)
print(sorted(members))
成员集合包含 close、get 和 name。函数返回的是不可变的 frozenset,集合没有业务顺序;如果要写日志、生成 JSON 或做快照测试,应先用 sorted() 得到稳定列表。
frozenset({'close', 'get', 'name'})
['close', 'get', 'name']
集合打印时的排列可能不同,这不代表成员变化。判断某个名称是否存在时直接使用集合成员测试即可:
required = get_protocol_members(Repository)
if "close" in required:
print("Repository 要求 close 成员")
五、继承与泛型场景怎么读取
协议继承正是这个 API 比手工反射可靠的地方。返回集合会覆盖协议类及其协议父类形成的成员要求,所以前例中的 close 不会丢失。
泛型协议需要注意传参对象。get_protocol_members 接受的是协议类;参数化后的 Store[int] 是泛型别名,不是要传给该函数的协议类。读取成员名称时传原始协议类 Store:
from typing import Protocol, TypeVar, get_protocol_members
T = TypeVar("T")
class Store(Protocol[T]):
def save(self, value: T) -> None: ...
print(sorted(get_protocol_members(Store)))
# 不要传 Store[int];它是参数化泛型别名,不是协议类本身。
# get_protocol_members(Store[int]) # TypeError
另一个边界是普通类。即便普通类恰好实现了 save,它也不是 Protocol 定义,传给函数仍会抛出 TypeError。需要接受不确定输入时,可以先用同在 Python 3.13 新增的 typing.is_protocol 判断。
from typing import get_protocol_members, is_protocol
def protocol_member_names(candidate: object) -> list[str]:
if not is_protocol(candidate):
raise ValueError(f"需要 Protocol 类,收到 {candidate!r}")
return sorted(get_protocol_members(candidate))

六、旧版本 Python 怎么保持同一接口
typing.get_protocol_members 从 Python 3.13 开始进入标准库。需要兼容更早 Python 的项目,可以使用 typing_extensions;该项目从 4.7.0 起提供同名回移实现,而且能处理由 typing.Protocol 或 typing_extensions.Protocol 定义的协议。
try:
# Python 3.13 及以上优先使用标准库。
from typing import Protocol, get_protocol_members, is_protocol
except ImportError:
# 旧版 Python 使用 typing_extensions 4.7.0 及以上。
from typing_extensions import Protocol, get_protocol_members, is_protocol
项目依赖应显式声明最低版本,而不是直接读取 _is_protocol、__protocol_attrs__ 等私有实现细节。公开函数的价值就在于让版本差异由标准库或兼容包处理。
七、加入防复发检查与适用边界
修复成员清单后,还要避免把这个函数扩展成它没有承诺的“运行时类型检查器”。下面几条可以作为防复发检查:
- 先判定输入:来源不可信时先调用
is_protocol,再读取成员。 - 固定输出顺序:日志、文档和快照测试统一使用
sorted(),不要依赖集合展示顺序。 - 传协议类:传
Store,不要传实例、普通实现类或Store[int]这样的泛型别名。 - 不要读取私有属性:不要把
__protocol_attrs__当作稳定业务接口。 - 不要误判兼容性:
get_protocol_members只返回名称,不比较实现类的方法签名与注解类型。
如果目标是检查一个对象能否在运行时满足协议,可以在协议上使用 @runtime_checkable 后调用 isinstance();但官方文档明确提醒,这类运行时检查只关注成员是否存在,不校验类型签名。需要严格验证兼容性时,仍应把 mypy、Pyright 等静态类型检查器放进开发或 CI 流程。
返回值为什么是 frozenset 而不是 list?
成员集合表达的是“有哪些名称”,不是声明顺序。frozenset 不可变,适合做包含判断和集合比较;需要可重复展示时再转换成排序列表。
它会返回父 Protocol 的成员吗?
会。协议成员在创建协议类时按其继承关系收集,因此读取子协议时能得到自身成员与协议父类带来的成员要求。
能用它判断某个类是否完整实现 Protocol 吗?
不能仅靠这个函数。它读取的是协议要求的名称,不检查另一个类,也不比较参数、返回值和属性类型。静态兼容性应由类型检查器判断;简单运行时存在性检查可结合 @runtime_checkable,但仍不等于签名验证。
归纳起来,读取协议成员的稳定写法是:先确认传入的是未参数化的 Protocol 类,再调用 get_protocol_members,展示时排序。这样既能保留继承成员和注解属性,也能避开 typing 私有实现变化带来的维护风险。
-
346 收藏
-
235 收藏
-
387 收藏
-
447 收藏
-
360 收藏
-
104 收藏
-
307 收藏
-
438 收藏
-
124 收藏
-
166 收藏
-
242 收藏
-
264 收藏
-
370 收藏
-
207 收藏
-
143 收藏
-
187 收藏
-
366 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习