在 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 的运行逻辑。

打开网易新闻 查看精彩图片

点赞有美意,赞赏是鼓励