数值运算协议(Numeric Operation Protocol)是 Python 在处理算术表达式时所遵循的一套核心规则。它规定了解释器在遇到 a + b、a * b、a ** b、-a、a += b 等语法结构时,应当如何判定参与对象是否可以建立运算语义,以及在判定成立后,沿何种严格顺序展开执行路径。
理解数值运算协议,关键不在于记住若干特殊方法名,而在于把握一个更根本的逻辑:
算术运算并不是对象主动执行的能力,而是解释器在算术语法语境下所采用的一套解释规则。
对象是否“支持加法”或“支持乘法”,并非其固有属性,而是解释器在特定语法结构中,对其类型结构应用数值运算协议后的判定结果。
一、什么是数值运算协议
在 Python 中,数值运算符有:
二元运算:+ - * / // % ** @ << >> & ^ |
一元运算:-x +x ~x abs(x)
就地运算:+= -= *= /= ...
当解释器遇到如下语法:
a + b它首先识别这是一次加法语法结构,然后触发数值运算协议解释路径。
数值运算协议是一套统一规则,但在不同运算符参与时,会沿不同的语义分支展开。尽管 + 与 * 语义不同,它们共享同一分派模型。
数值运算协议并不是运行期实体,也不是某种接口类型。它存在于解释器的规则体系之中。
二、数值运算协议的方法构成
数值运算协议涉及以下几类核心方法。
1、二元运算方法(左侧路径)
__or__(self, other) # |2、反向二元运算方法(右侧兜底路径)
__ror__(self, other) # |3、就地运算方法(优先路径)
__ior__(self, other) # |=4、一元运算方法
__invert__(self) # ~x这些方法本身只是定义在类中的普通函数对象。它们不会因为名称特殊而自动生效。
数值运算协议不通过实例属性查找流程(即不会触发 __getattribute__ 或 __getattr__)。解释器在运算语境下直接从类型对象的数值槽位结构(PyNumberMethods)读取对应入口。
三、自定义数值运算(以加法为例)
数值运算协议允许用户通过定义特殊方法参与算术语义的建立。以下以加法运算为例说明。
print(v3.x) # 7自定义运算并不是“给对象添加一个能力”,而是在特定运算语境(如 v1+ v2)中,为解释器提供一个可被选中的协议入口。
四、实例属性不会影响数值运算协议
与调用协议、比较协议一致,数值运算协议的判定发生在类型层面,而不是实例字典。
例如:
a + b # TypeError尽管实例 a 拥有名为 __add__ 的属性,但解释器在加法语境中不会通过普通属性访问机制进行判定,而是读取类型层的数值运算入口。
因此,运算能力并不来自实例属性,而来自类型结构中的协议实现。
五、二元运算的严格顺序(以加法为例)
对于表达式:
a + b其解释路径(严格顺序)为:
1、解释器首先检查 type(b) 是否是 type(a) 的真子类:
issubclass(type(b), type(a)) and type(a) != type(b)→ 若是, 按子类优先原则, 读取 type(b) 的数值运算槽位, 优先尝试右侧反向路径:
type(b).__radd__(b, a)→ 若不是,则按以下的左侧正向路径展开。
2、读取 type(a) 的数值运算槽位(在 CPython 中为 PyNumberMethods.nb_add),调用。
type(a).__add__(a, b)3、若正向路径返回 NotImplemented,则尝试反向路径:
type(b).__radd__(b, a)4、若所有路径均返回 NotImplemented,抛出 TypeError。
(1)解释器首先把运算视为“左操作数主导的运算尝试”,因此第一候选路径以左操作数的数值槽位作为入口,常称为“正向路径”或“主导路径”。
(2)还有一条候选路径是以右操作数的数值槽位作为入口,称为“反向路径”或“协议兜底路径”。
(3)先尝试正向路径,若失败再尝试反向路径,这是数值运算协议的最核心的分派模型。
(4)返回 NotImplemented 表示“该类型无法处理该组合”,从而允许解释器继续尝试其他候选路径。
(5)数值运算协议支持子类优先规则。当右操作数类型是左操作数类型的真子类时,解释器优先调用右侧类型的反向方法,以保证子类可以覆盖父类运算语义。
因此,a + b 并不是简单调用某个方法,而是在加法语境中触发一次受规则控制的协议分派。
其他二元运算(- * / // % ** @ << >> & ^ |)均遵循同样结构,只是对应不同槽位与方法名。
协议分派发生在类型层数值槽位结构中,而不是通过常规属性访问流程,因此它不是通过实例属性查找机制触发的方法调用,而是由解释器在运算语境中直接读取类型槽位并完成的协议分派。
六、一元运算的解释路径
对于表达式:
-a解释路径为:
1、读取 type(a) 的对应一元运算槽位(如 nb_negative);
2、调用:
type(a).__neg__(a)3、若不存在该入口,抛出 TypeError。
一元运算不涉及反向协商,因此不存在双向路径。
七、就地运算的优先分支
表达式:
a += b其解释路径为:
1、读取 type(a) 的就地运算槽位(在 CPython 中表现为 nb_inplace_add),调用:
type(a).__iadd__(a, b)2、若不存在该入口或返回 NotImplemented,则回退为普通二元运算路径(见前面章节)。
3、最终将结果重新绑定到变量名。
需要强调的是:
• “就地”并不保证原地修改对象
• 是否原地修改完全由类型实现决定
• 协议仅规定分派顺序。
八、位运算与矩阵运算
位运算(& ^ | << >>)与矩阵乘法(@)在协议结构上完全遵循与加法相同的分派模型:
• 读取对应数值槽位
• 先尝试正向路径,若失败再尝试反向路径
• 子类优先插队规则
• NotImplemented 协商
• 失败抛出 TypeError
它们不是独立机制,而是数值运算协议的不同分支。
九、数值运算协议的典型应用场景
数值运算协议贯穿 Python 语言的多个核心机制:
1、算术表达式计算
2、数值类型模拟(自定义向量、矩阵等)
3、就地修改语义
4、位运算与底层数据结构操作
5、框架中重载运算符的 DSL 设计
这些场景的共同点在于:对象并未“执行运算”,而是在算术语法语境下,被解释器按协议规则解释为参与运算的操作数。
小结
数值运算协议不是对象模型中的实体,而是一组由解释器遵循的运算分派规则。它规定了解释器在运算语法语境中如何判定对象是否可参与运算、如何在类型结构中寻找语义入口,以及在协商失败时如何处理。对象是否“支持某种运算”,并不取决于名称或类别标签,而取决于解释器在特定语境下对其类型结构应用数值运算协议后的结果。
![]()
“点赞有美意,赞赏是鼓励”
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.