尧图精选

Python魔法方法解析:理解__rfloordiv__实现反向整除运算

🕒 发布时间:2026/9/26 20:58:03 📁 来源:尧图网络
写 Python 写了这么多年我越来越觉得魔法方法是这门语言最值得花时间琢磨的部分。看起来只是在敲3 // 2但 CPython 会在背后做一次完整的操作数分派先找左操作数的__floordiv__不行再找右操作数的__rfloordiv__。很多人实现了前者却完全没注意到后者直到某天写出some_int // my_object被 TypeError 砸脸才开始翻文档。这篇想认真聊聊 Python 3.12 魔法方法里的__rfloordiv__它解决什么问题、CPython 的调用顺序是什么、怎么在一两个自己的类里正确落地以及有哪些容易进坑的边界情况。内容适合两种读者一种是想系统补完魔法方法知识的进阶学习者另一种是维护自己写的数值类型或容器类型遇到TypeError: unsupported operand type(s) for //想搞明白为什么的实践派。1. 整除运算符的“第二站位”rfloordiv为什么存在1.1 一个只有floordiv的类会漏掉什么先想象一个常见的困境。你写了一个区间类Interval用来表示整数区间并且实现了正向整除class Interval: def __init__(self, start, end): self.start start self.end end def __floordiv__(self, other): if not isinstance(other, int): return NotImplemented step (self.end - self.start) // other return [self.start i * step for i in range(other 1)]interval // 3用起来完全正常能把区间切成四段。但某天你脑子里冒出一个想法3 // interval能不能表达“这个区间大致能容纳 3 的多少个整数跨度”一试直接TypeError: unsupported operand type(s) for //: int and Interval。问题出在3 // interval的执行路径上。CPython 首先尝试int.__floordiv__(3, interval)int 类型根本不认识你的 Interval于是返回 NotImplemented接着 CPython 转头尝试Interval.__rfloordiv__(interval, 3)但你的类里没定义这个方法于是整个表达式以 TypeError 收场。__rfloordiv__就是给位于运算符右侧的自定义对象一个补救机会。名字里的r在官方文档里叫 reflected翻译过来是“反射”或“反向”而不是 right右侧。虽然直觉上它像是替右操作数说话但严格的称呼是“反射版本”。这个反射版本之所以重要是因为它专门处理“左操作数不擅长、右操作数反而更懂”的情况。1.2 反射方法家族与命名规律__rfloordiv__不是孤例Python 为几乎所有二元算术运算符都配了反射版本规律极其简单在正向方法名前面加一个r。下表是完整清单收藏起来以后翻很方便正向方法反射方法运算符__add____radd____sub____rsub__-__mul____rmul__*__truediv____rtruediv__/__floordiv____rfloordiv__//__mod____rmod__%__pow____rpow__**__matmul____rmatmul____lshift____rlshift____rshift____rrshift____and____rand____or____ror__|__xor____rxor__^为什么整除、减法、除法这些运算必须有反射方法因为它们是非交换运算。a // b和b // a的语义完全两回事不能简单地把两个参数对调后复用正向方法当左侧类型处理不了时右侧类型必须用自己的规则从“另一个视角”重新解释这次运算。至于加法明明交换律成立Python 也保留了__radd__原因是自定义类可能希望在number my_object这种写法里接管计算从而让你的类型表现得更自然。2. 操作数分派规则全解先问左边再问右边子类还有优先权2.1 一条运算的两个机会正向与反射的完整查找顺序如果你想真正理解__rfloordiv__就不能只看名字得把 CPython 对二元运算符的分派顺序背下来。忽略一些内置类型的细节left // right的逻辑可以用一段伪代码概括def apply_floordiv(left, right): result type(left).__floordiv__(left, right) if result is not NotImplemented: return result result type(right).__rfloordiv__(right, left) if result is not NotImplemented: return result raise TypeError( funsupported operand type(s) for //: f{type(left).__name__} and {type(right).__name__} )注意反射方法的参数顺序__rfloordiv__(self, other)里self 是原表达式中的右操作数other 才是原表达式中的左操作数。比如2 // my_obj调用的是my_obj.__rfloordiv__(2)此时 self 是 my_objother 是 2。很多初学者在实现反射方法时习惯性把参数写反算出来的结果莫名其妙就是因为这块没理清。还有一个非常容易被忽略的点正向方法返回什么决定了反射方法有没有出场机会。判断标准不是“返回了值”而是“返回的值是不是NotImplemented这个对象”。只有正向方法返回NotImplemented反射方法才会被尝试。如果正向方法直接抛了 TypeError反射方法连出场机会都没有。2.2 NotImplemented 是关键枢纽不是报错Python 里有两个看起来很像、作用完全不同的东西NotImplemented和NotImplementedError。每次讲反射方法都要强调一次因为几乎所有新手都会在这里混淆。NotImplemented是一个单例对象它的类型是NotImplementedType。它不是一个异常类你不能raise NotImplemented也不应该return NotImplementedError。它的作用是告诉 Python 解释器“当前这个方法不支持这个操作数组合但你可以再问问另一方的反射方法。” 在反射方法里同样如此如果反射方法也返回NotImplementedCPython 才会最终抛出那个我们熟悉的 TypeError。正确用法是返回值判断用is因为NotImplemented是单例def __floordiv__(self, other): if not isinstance(other, (int, float)): return NotImplemented return CustomValue(self.value // other)而NotImplementedError是异常类通常用在抽象基类里表示“这个方法应该由子类实现”。一旦你真把NotImplementedError从__floordiv__里抛出去那就是普通异常会直接中断程序反射方法也没机会执行。这两件事必须分清楚。2.3 子类操作数优先规则一个容易忽略的分支常规顺序是“先正向后反射”但 CPython 里有一个例外很多人写复杂类型时会栽跟头如果右操作数的类型是左操作数类型的子类那么反射方法会打破“先左后右”的顺序被优先尝试。这个规则的出发点很朴素子类代表更具体的类型理应拥有更高的优先级。看一个具体的反直觉例子class Number: def __floordiv__(self, other): return Number.__floordiv__ class SmallNumber(Number): def __rfloordiv__(self, other): return SmallNumber.__rfloordiv__ n Number() s SmallNumber() print(n // s) # SmallNumber.__rfloordiv__ print(s // n) # Number.__floordiv__第一个n // s里左操作数 n 是 Number右操作数 s 是 SmallNumber。右操作数的类型 SmallNumber 是左操作数类型 Number 的子类所以 CPython 优先调用SmallNumber.__rfloordiv__(s, n)返回SmallNumber.__rfloordiv__尽管 Number 的__floordiv__完全有能力处理这次运算。第二个s // n左操作数是 SmallNumber右操作数是 Number。右操作数的类型不是左操作数类型的子类于是回到常规顺序先执行继承自 Number 的__floordiv__返回Number.__floordiv__。这就是为什么我说它反直觉同一个类组合把两个对象的位置调换结果完全不一样。这个规则在实现子类时特别重要。如果你的自定义类型存在继承关系千万别只测试“父类和子类分别和普通整数运算”一定要把父子类互相运算的情况也测一遍否则很容易出现 “子类放在右边时抢走了父类的位置” 这种意外。3. 手写一个支持双向整除的例子Vec2D 从 0 到 13.1 先做能跑的正向整除理论说了这么多不如动手实现一个完整的例子。我先做一个极简的二维向量类Vec2D目标很简单支持vector // scalar也就是把向量的每个分量都对 scalar 做向下取整除法。class Vec2D: def __init__(self, x, y): self.x x self.y y def __floordiv__(self, other): if isinstance(other, Vec2D): return Vec2D(self.x // other.x, self.y // other.y) if isinstance(other, (int, float)): return Vec2D(self.x // other, self.y // other) return NotImplemented def __repr__(self): return fVec2D({self.x}, {self.y})这个正向实现里有几个细节值得说。第一Vec2D // Vec2D我保留了逐元素整除的语义第二Vec2D // int或// float做标量整除第三除此之外的所有类型我一律return NotImplemented绝不自己抛 TypeError。这是给反射方法留路也是上面第二章讲的正确姿势。验证一下v Vec2D(7, 8) print(v // 2) # Vec2D(3, 4) print(v // Vec2D(2, 3)) # Vec2D(3, 2)结果符合预期7 // 2 38 // 2 4。但如果这时候直接跑2 // v结果必然是TypeError: unsupported operand type(s) for //: int and Vec2D。这就轮到__rfloordiv__上场了。3.2 再补上rfloordiv别踩递归目标场景是支持scalar // vector语义定义成“标量对向量的每个分量反向整除”也就是Vec2D(other // self.x, other // self.y)。代码写出来def __rfloordiv__(self, other): if isinstance(other, (int, float)): return Vec2D(other // self.x, other // self.y) return NotImplemented注意参数顺序这里 self 是 Vec2D 实例也就是原表达式里的右操作数other 是标量才是原表达式里的左操作数。所以计算时是other // self.x不是self.x // other。这个顺序写反等于把2 // v变成了v.x // 2语义就反了。一个我亲眼看过很多次的错误写法是def __rfloordiv__(self, other): return other // self看起来简洁实际是个灾难。other // self会在 Python 内部重新走一遍完整的运算符分派先尝试int.__floordiv__(other, self)int 不认识 Vec2D返回 NotImplemented然后因为 self 是 Vec2D而 Vec2D 定义了__rfloordiv__(self, other)于是又回到这个方法里方法体又写出other // self于是无限递归下去最终RecursionError: maximum recursion depth exceeded。这是反射方法中最经典的递归陷阱。反射方法里必须手写计算逻辑不能用“再调用一次运算符”的懒办法除非你明确知道另一方的类型一定能处理。3.3 多测几个方向顺便看看准确语义补上反射方法后再验证这次把正反两个方向都测一遍v Vec2D(7, 8) print(v // 2) # Vec2D(3, 4) print(2 // v) # Vec2D(0, 0) print(2.7 // v) # Vec2D(0.0, 0.0) print(v // v) # Vec2D(1, 1)2 // v的结果是Vec2D(0, 0)因为2 // 7 02 // 8 0。向下取整除法的本质决定了这就是正确结果2 除以 7 商 0余 2。2.7 // v则因为浮点数参与返回了浮点型0.0Python 的//对浮点数的语义本来如此。还可以用operator.floordiv验证它能绕开语法层面的干扰但分派逻辑一样import operator v Vec2D(7, 8) print(operator.floordiv(2, v)) # Vec2D(0, 0)如果此时删除__floordiv__只保留__rfloordiv__v // 2会变成 TypeError而2 // v依然正常工作。这也是判断一个类对运算符支持是否完整的好办法正向和反射各测一遍缺哪个补哪个。4. 三个“整除家人”的关系floordiv、rfloordiv与ifloordiv4.1ifloordiv的就地优先聊完普通运算符得聊聊//。a // b看起来只是a a // b的语法糖但对自定义类来说CPython 会优先寻找__ifloordiv__找不到或返回 NotImplemented 时才会退化成普通除法。写个例子感受一下优先级class Counter: def __init__(self, value): self.value value def __ifloordiv__(self, other): print(in-place floordiv called) self.value // other return self def __floordiv__(self, other): print(normal floordiv called) return Counter(self.value // other) c Counter(17) c // 3 print(c.value) # 5输出只会有in-place floordiv called说明//直接走就地方法根本没有经过__floordiv__。这也符合直觉原地运算就是为了避免创建新对象。4.2 就地方法失败后的退化路径如果类没有__ifloordiv__或者__ifloordiv__返回了NotImplemented//不会立刻报错而是退化成a a // b这时才会触发普通的分派流程也就是我们前面说的__floordiv__和__rfloordiv__组合拳。这带来一个有意思的推论a // b在“a 没有就地方法”的前提下如果 b 的类型正好是 a 类型子类并且实现了__rfloordiv__那么反射方法也会被调用。所以反射方法不是只在“普通表达式”里有用就地赋值表达式同样可能触发它。class Wrapper: def __init__(self, data): self.data data def __floordiv__(self, other): if not isinstance(other, Wrapper): return NotImplemented return Wrapper(self.data // other.data) w Wrapper(17) w // 3这里没有__ifloordiv__w // 3退化成w w // 3。Wrapper.__floordiv__发现 other 不是 Wrapper返回 NotImplemented随后 CPython 尝试int.__rfloordiv__(3, w)int 同样不认识 Wrapper最终 TypeError。想要原地整除一个内部标量必须自己显式定义__ifloordiv__不能指望反射方法帮你完成。4.3 三者职责定位与设计倾向把三个方法放在一起看职责其实很清楚方法触发场景返回值语义__floordiv__a // b的左操作数返回新结果或返回 NotImplemented__rfloordiv__a // b的左操作数处理不了轮到右操作数返回新结果或返回 NotImplemented__ifloordiv__a // b的左操作数原地运算返回的值会成为新的 a通常返回 self从设计角度讲不是三个都要实现。如果你的类型只是偶尔出现在表达式的右边只写__rfloordiv__就够了如果它经常作为左边操作数并被整除写__floordiv__如果它还有可变状态希望//能原地更新再补__ifloordiv__。三者的共同底线是不支持的组合一律return NotImplemented而不是抛异常。5. 工程中的取舍与观察哪些场景才值得实现反向整除5.1 真实世界里的rfloordiv应用说实话在纯应用层开发里自定义__rfloordiv__的需求没有__radd__或__rtruediv__那么高频因为大部分业务类根本不参与int // MyClass这种运算。真正常见的位置是数值类、向量类、时间区间类以及科学计算里的自定义张量包装。比如 NumPy 的 ndarray 就完整实现了反射算术协议所以你能直接写import numpy as np arr np.array([3, 5]) print(10 // arr) # [3 2]这个结果就是 ndarray 内部的反射槽在起作用——左操作数 int 处理不了int // ndarray于是把控制权交给了 ndarray 的反射方法。你在 pandas 里写scalar // Series遇到的也是同一套机制。如果你在设计的是量纲类比如Length、TimeSpan、Money这类带单位的值对象反射整除往往有非常自然的语义。length // n表示把长度等分成 n 份而n // length可以表示“这段总长里能容纳多少个该长度单位”两者含义完全不同都需要精心实现。这种情况下__rfloordiv__就不是“顺手补上”而是这个类 API 的一部分必须像设计正向方法一样认真对待。5.2 Python 3.12 下的实现建议Python 3.12 里__rfloordiv__的行为和 Python 3.6、3.8 基本一致协议非常稳定。不过写代码时可以用上现代类型注解风格尤其推荐typing.SelfPython 3.11 之后的标准做法from typing import Self class Vec2D: def __rfloordiv__(self, other: object) - Self: if isinstance(other, (int, float)): return Vec2D(other // self.x, other // self.y) return NotImplemented # type: ignore把参数标注成object然后在方法里用 isinstance 做类型窄化这是最符合 Python 风格的做法。反射方法尤其适合这样写因为你完全不知道左操作数会是什么类型只有运行时检查才靠谱。另外提一句从 Python 3.12 开始CPython 内部对 int 的整除和幂运算做了一些性能优化但这些优化集中在内置类型层面和我们自定义的魔法方法没有关系。你该关注的只是协议本身。5.3 我自己的几条经验规则踩过不少坑之后我给自己总结了几条关于实现反射整除的规则也顺手分享给看到这里的人。第一先问“反向有没有语义”再决定要不要实现。2 // my_obj如果根本没有明确含义就不要硬写。没人会为了“让 TypeError 不出现”而给一个语义不明的类塞反射方法那只会让代码更难懂。第二反射方法的other永远是左操作数。遇到scalar // obj就写scalar // obj.x不要写obj.x // scalar。参数顺序错了程序不报错但结果全是错的这种 bug 最难排查。第三不支持的运算请返回NotImplemented别返回None。None会被当成有效结果继续使用然后在下一次属性访问时炸出莫名其妙的 AttributeError。返回NotImplemented才能让 CPython 正确判断“此路不通”。第四写完反射方法后把左操作数、右操作数、子类继承、浮点数参与这四种情况全部测一遍。我自己的测试习惯是多写一个方向的反例比如测试2.5 // v是否按浮点语义返回测试abc // v是否正确抛 TypeError这些边界情况能帮你确认返回 NotImplemented 的分支真的生效了。第五能不用运算符就别硬用运算符。在反射方法内部宁可写清楚other // self.x也不要图省事写other // self。前面已经演示过那会直接递归爆栈。如果你确实需要复用正向逻辑提取一个私有方法在里面传参计算而不是再触发一轮运算符分派。把这些规则记在心里__rfloordiv__就从“一个冷门魔法方法”变成了“一座好用的桥”。它存在的意义不是让你把每个运算符都实现一遍而是让你的自定义类型在承担右操作数身份时依然能完整表达自己那一边的语义。我个人的体会是真正把反射方法想明白了你就会开始留意到 Python 中每一个二元运算符背后都有一套精密的谦让机制——左边不行就让右边右边不行就报错整个过程安静、稳定很少有歧义。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →