在 Python 中,语法结构本身并不直接规定对象如何执行行为。语法只确定语言结构,而具体的运行期行为,则由解释器根据既定规则进行解释与分派。这套规则体系,就是“协议机制”。
理解协议机制,关键不在于记忆若干特殊方法名,而在于把握一个核心逻辑:语法结构决定采用哪一种解释规则;对象的类型结构决定是否可以启用该规则,并决定解释路径的入口。
协议不是对象主动执行的行为,而是解释器在特定语法语境下所采用的解释规则集合。
一、什么是协议
在 Python 中,“协议”(protocol)不是对象、不是类型、也不是接口。它不作为独立实体存在,而是解释器在某种语法结构下,用以展开运行期行为的一套规则。
从是否允许对象参与规则实现的角度,协议可以分为两类:
1、用户可扩展的规则
对象可以通过在类型上定义特殊方法(“魔术方法”)参与该规则的实现。这些方法可称为“协议方法”。
2、用户不可扩展的规则
规则完全由解释器内建,不提供对象侧的结构性入口(即对象无法“定义/接管”这条规则的关键步骤)。
通常所说的“协议机制”,指的是允许对象通过协议方法参与解释路径展开的可扩展规则。
二、协议机制的统一工作流程
所有可扩展协议,都遵循同一套运行模型。
1、语法触发
解释器首先识别语法结构,例如:
with obj语法结构决定解释器将实施哪种规则:
• 调用表达式 →
• 数值运算 →
• for 语句 →
• 点运算 →
• with 语句 →
语法结构只触发选择规则类别,并不决定具体行为。
2、类型入口判定(槽位检查)
在确定协议类别后,解释器进入类型层分派阶段。
首先要解决的问题是:当前对象的类型是否为该协议的解释路径提供入口?
在 CPython 中,这种入口表现为类型对象中的(type slots)。
例如:
• 调用协议 → tp_call
• 迭代协议 → tp_iter
• 数值协议 → tp_as_number->nb_add
• 属性访问 → tp_getattro
• 描述符 → tp_descr_get / tp_descr_set
槽位是类对象内部结构中预先填充的函数指针字段。比如:
...在类对象创建或更新时,解释器会将该方法映射到对应槽位(如 tp_call)。因此,槽位中保存的是解释器层的 C 函数指针,该函数在内部再分派到相应的协议方法(若存在)。
语法结构触发解释器进入对应的协议分派逻辑,该逻辑会从对象类型结构中读取相关槽位。
若协议入口槽位为空,解释器将根据该协议的定义尝试是否存在回退路径;若无可用路径,才会抛出 TypeError。
因此,协议解释路径是否能展开,取决于类型结构是否提供入口以及协议内部规则是否允许展开。
3、解释路径展开
若槽位非空,解释器立即进入协议解释路径:
1、通过槽位调用对应的解释器层分派函数,该函数在内部再调用协议方法(若存在)
2、获取返回值
3、若返回 NotImplemented,尝试替代路径(若协议中存在)
4、若所有路径都尝试失败,抛出 TypeError
例如调用表达式:
obj()解释路径展开如下:
1、读取 type(obj) 的 tp_call 槽位,通过该槽位对应的调用函数展开调用路径;在普通对象情况下,该路径会进一步分派到 __call__ 方法。
2、返回其结果。
三、解释路径优先级与回退规则
部分协议存在多路径竞争机制。
比如,数值运算协议中包含原地方法、正向方法以及反向方法等,支持多路径竞争。
例如:
a + b解释路径(严格顺序)为:
1、读取 type(a) 的加法槽位 nb_add,调用相关协议方法 type(a).__add__(b)。
2、若返回 NotImplemented,则尝试反向路径(右操作数兜底)type(b).__radd__(a)。
若 type(b) 是 type(a) 的严格子类,则根据数值协议规则,解释器会优先尝试 type(b).__radd__(a),以保证子类优先。
3、若所有候选仍返回 NotImplemented,抛出 TypeError。
又如原地计算:
a += b解释路径为:
1、读取 type(a) 的原地加法槽位 nb_inplace_add,调用相关协议方法 type(a).__iadd__(b)。
2、若返回 NotImplemented,回退到上述普通加法解释路径。
在回退发生时,其行为语义等价于执行:
a = a + b要强调的是,NotImplemented 不是错误。
它是协议协作信号:“我不能处理这个组合,请尝试其它候选路径。”
只有当所有候选路径都返回 NotImplemented,解释器才抛出 TypeError。
因此,协议不仅定义入口,也定义路径优先级与回退规则。
四、协议解释路径何时不会生效
协议是否最终生效,并不只取决于槽位是否非空。
在解释器运行过程中,还存在若干情形会导致协议路径不被触发、被遮蔽或被替代。
1、语法结构未触发该协议
协议必须由对应语法结构触发。
即便类型提供了入口槽位,如果对象从未出现在该语法语境中,协议解释路径就不会被触发。
例如:
如果从未用于:
...或:
iter(X())则迭代协议根本不会触发。
2、类型槽位为空
例如:
a()调用表达式触发调用协议,但 int 实例所属类型不提供实例级调用入口槽位,因此解释路径无法展开。
这是“入口不存在”的情况。
3、回退路径替代原路径
某些协议包含“主路径 + 回退路径”。
即便主槽位存在,若返回 NotImplemented,解释器会尝试替代路径。
例如前面所讲的数值协议中,正向路径可能被反向路径替代,或者原地路径被普通加法路径替代。
还有一种可能引发槽位级回退的情形。比如:
for x in obj:正常情况读取 type(obj) 的“迭代”槽位 tp_iter,调用相关协议方法 type(obj).__iter__()
若对象未提供 tp_iter 槽位,但实现了序列槽位(sq_item),解释器会构造一个序列迭代器对象,由其逐次调用 sq_item,直到抛出 IndexError 结束。
4、MRO 改变协议入口的实现来源
在继承场景中,协议路径可能被子类覆盖。
print(len(B())) # 2最终槽位绑定到 B.__len__。这里并不是遮蔽,而是类型结构本身发生变化。
小结
在 Python 中,协议是解释器在特定语法语境下采用的解释规则体系。语法结构决定协议类别,类型结构决定是否提供入口,协议内部规则决定解释路径如何展开及如何竞争与回退。协议方法只是对象参与协议的语言接口,真正的行为分派发生在类型层。理解协议机制,就是从解释器的角度理解 Python 的运行逻辑。
“点赞有美意,赞赏是鼓励”
热门跟贴