Python 3.12 Magic Method - __and__(self, other)
__and__ 是 Python 中用于定义 按位与运算符 & 的核心魔术方法。它最常见的用途是实现整数的位与运算,但也可以被重载用于任何需要"逻辑与"或"交集"语义的操作,例如集合的交集、标志位组合等。正确实现 __and__ 可以让自定义类支持 & 运算,并与其他位运算符(如 |、^、~)保持一致的语义。本文将详细解析其定义、底层机制、设计原则,并通过多个示例逐行演示如何正确实现。
1. 定义与签名
python
def __and__(self, other) -> object:
...
- 参数 :
self:当前对象(左操作数)。other:另一个操作数(右操作数),可以是任意类型。
- 返回值 :应返回一个新的对象,代表按位与运算的结果。如果运算未定义(例如类型不兼容),应返回单例
NotImplemented。 - 调用时机 :
x & y会首先尝试调用x.__and__(y)。- 如果
x.__and__(y)返回NotImplemented,Python 会尝试调用y.__rand__(x)(反向按位与)。 - 如果两者都返回
NotImplemented,最终抛出TypeError。
2. 用途与典型场景
- 整数位运算 :自定义整数类型(如大整数、有限域元素)支持
&运算。 - 自定义集合 :实现集合的交集操作,类似内置
set的&运算符。 - 位标志组合:处理权限标志、配置选项等。
- 自定义容器:可用于过滤元素,保留两个容器中共有的部分。
按位与运算通常满足交换律,因此正向和反向运算结果一致,但 __rand__ 仍需要实现以处理左操作数为其他类型的情况。
3. 底层实现机制
在 Python/C API 层面,每个类型对象(PyTypeObject)都有一个 tp_as_number 结构体,其中包含 nb_and 槽位,这是一个函数指针,用于处理按位与操作。当执行 x & y 时,解释器会:
- 获取
x的类型对象的tp_as_number结构。 - 如果存在
nb_and,则调用它,传入x和y,返回结果对象或Py_NotImplemented。 - 如果
x的nb_and返回Py_NotImplemented,则尝试获取y的类型对象的nb_and,并调用它,但此时参数顺序已交换(即调用y的__rand__对应的 C 函数)。 - 如果仍然失败,则抛出
TypeError。
对于 Python 层定义的 __and__,它会被包装到 nb_and 槽位中。反向方法 __rand__ 也会在必要时被调用,用于处理左操作数不支持该运算的情况。
4. 设计原则与最佳实践
- 遵循数学/语义一致性 :对于整数类型,按位与应满足
a & b的每一位为1当且仅当a和b的对应位均为1。对于自定义集合,应定义为交集。 - 返回新对象 :
__and__通常不应修改操作数本身,而应返回一个新对象,以保持不可变性(除非你明确设计为就地修改,但通常这不是按位与的预期行为)。 - 类型检查 :应检查
other的类型是否兼容,如果类型不匹配,应返回NotImplemented,而不是抛出异常。这给另一操作数提供尝试反向运算的机会。 - 与
__rand__和__iand__的协作 :- 由于按位与满足交换律,
__rand__可以直接委托给__and__(或直接实现相同逻辑)。 __iand__用于就地按位与(&=),通常应修改自身并返回self,适用于可变对象。
- 由于按位与满足交换律,
- 处理不同类型 :如果希望支持混合类型运算(如自定义类与整数),应在
__and__和__rand__中适当处理。
5. 示例与逐行解析
示例 1:自定义整数类(支持按位与)
python
class MyInt:
def __init__(self, value):
self.value = value
def __and__(self, other):
# 处理 MyInt & 其他类型
if isinstance(other, MyInt):
return MyInt(self.value & other.value)
if isinstance(other, int):
return MyInt(self.value & other)
return NotImplemented
def __rand__(self, other):
# 处理 其他类型 & MyInt
if isinstance(other, int):
return MyInt(other & self.value)
return NotImplemented
def __repr__(self):
return f"MyInt({self.value})"
逐行解析:
| 行 | 代码 | 解释 |
|---|---|---|
| 1-3 | __init__ |
初始化整数值。 |
| 4-9 | __and__ |
正按位与。 |
| 5-7 | 处理同类型 | 如果 other 是 MyInt,使用内置 & 计算,返回新 MyInt。 |
| 8 | 处理整数 | 如果 other 是整数,直接使用 self.value & other。 |
| 9 | 返回 NotImplemented |
类型不支持时返回 NotImplemented。 |
| 10-13 | __rand__ |
反向按位与,处理 int & MyInt。由于满足交换律,可直接复用逻辑。 |
| 14-15 | __repr__ |
便于显示。 |
为什么这样写?
__and__通过类型检查处理不同情况。- 由于按位与满足交换律,
__rand__可以直接调用self & other吗?注意__rand__中self是右操作数,other是左操作数,所以self & other会再次触发__and__(self是MyInt,other是int),而我们的__and__已经处理了MyInt & int,所以直接返回self.__and__(other)即可。但要注意递归风险,不过这里不会递归,因为__and__会正确返回新对象。我们这里选择直接实现逻辑,但委托也是可行的。 - 返回新对象,保持不可变性。
验证:
python
a = MyInt(5) # 二进制 101
b = MyInt(3) # 二进制 011
print(a & b) # MyInt(1) (101 & 011 = 001)
print(a & 6) # MyInt(4) (101 & 110 = 100)
print(7 & a) # MyInt(5) (111 & 101 = 101) 通过 __rand__
运行结果:
MyInt(1)
MyInt(4)
MyInt(5)
示例 2:自定义位集合类(实现交集)
python
class BitSet:
def __init__(self, bits):
self.bits = set(bits) # 使用集合存储元素(假设是可哈希的)
def __and__(self, other):
# 交集操作:返回新集合
if isinstance(other, BitSet):
return BitSet(self.bits & other.bits)
return NotImplemented
def __rand__(self, other):
# 如果 other 是普通 set,也允许运算
if isinstance(other, set):
return BitSet(self.bits & other)
return NotImplemented
def __repr__(self):
return f"BitSet({self.bits})"
逐行解析:
| 行 | 代码 | 解释 |
|---|---|---|
| 1-3 | __init__ |
初始化内部集合。 |
| 4-7 | __and__ |
正交集操作,返回新 BitSet。 |
| 8-11 | __rand__ |
反向交集,处理 set & BitSet。 |
| 12-13 | __repr__ |
便于显示。 |
为什么这样写?
__and__利用内置集合的&运算,直接得到交集。__rand__允许内置集合左运算,提升灵活性。
验证:
python
bs1 = BitSet({1, 2, 3})
bs2 = BitSet({2, 3, 4})
print(bs1 & bs2) # BitSet({2, 3})
print({1, 3, 5} & bs1) # BitSet({1, 3}) 通过 __rand__
运行结果:
BitSet({2, 3})
BitSet({1, 3})
示例 3:权限标志位组合
python
class Permissions:
READ = 0b100
WRITE = 0b010
EXECUTE = 0b001
def __init__(self, flags=0):
self.flags = flags
def __and__(self, other):
if isinstance(other, Permissions):
return Permissions(self.flags & other.flags)
if isinstance(other, int):
return Permissions(self.flags & other)
return NotImplemented
def __rand__(self, other):
if isinstance(other, int):
return Permissions(other & self.flags)
return NotImplemented
def has(self, perm):
return (self.flags & perm) == perm
def __repr__(self):
return f"Permissions({'0b' + bin(self.flags)[2:].zfill(3)})"
解析 :
这里 & 用于提取权限位,实现简洁。
验证:
python
p = Permissions(Permissions.READ | Permissions.WRITE)
print(p & Permissions.READ) # Permissions(0b100)
print(p & 0b010) # Permissions(0b010)
print(0b001 & p) # Permissions(0b000)
运行结果:
Permissions(0b100)
Permissions(0b010)
Permissions(0b000)
示例 4:实现就地按位与(__iand__)
python
class MyInt:
def __init__(self, value):
self.value = value
def __iand__(self, other):
if isinstance(other, (int, MyInt)):
mask = other.value if isinstance(other, MyInt) else other
self.value &= mask
return self
return NotImplemented
def __and__(self, other):
if isinstance(other, (int, MyInt)):
mask = other.value if isinstance(other, MyInt) else other
return MyInt(self.value & mask)
return NotImplemented
def __str__(self):
return f"MyInt({self.value})"
解析 :
__iand__ 直接修改 self.value 并返回 self。
验证:
python
a = MyInt(13) # 1101
a &= 7 # 1101 & 0111 = 0101
print(a) # MyInt(5)
运行结果:
MyInt(5)
6. 与 __rand__ 和 __iand__ 的关系
| 方法 | 作用 | 典型返回值 | 调用时机 |
|---|---|---|---|
__and__(self, other) |
正向按位与 self & other |
新对象 | x & y |
__rand__(self, other) |
反向按位与 other & self |
新对象 | 正向返回 NotImplemented 时 |
__iand__(self, other) |
就地按位与 self &= other |
self |
x &= y |
关键区别:
__rand__用于左操作数不支持运算时的反向调用,由于按位与满足交换律,可以复用正向逻辑。__iand__用于原地修改对象,应返回self,适用于可变对象。
7. 注意事项与陷阱
- 不要修改
self:__and__应返回新对象,除非你明确设计为就地修改(此时应使用__iand__)。 - 正确使用
NotImplemented:当类型不兼容时返回NotImplemented,而不是抛出异常。这给另一侧机会处理。 - 与
__rand__的对称性 :虽然按位与可交换,但仍需实现__rand__以支持左操作数为其他类型。如果忘记实现,混合类型运算将失败。 - 与
__iand__的区分 :对于可变对象,应同时实现__iand__以支持&=,否则会回退到__and__并重新绑定,可能产生新对象。 - 避免无限递归 :在
__rand__中调用self & other可能会递归,但若__and__能正确处理该类型,则不会。更安全的做法是直接实现运算逻辑,而不是委托。
8. 总结
| 特性 | 说明 |
|---|---|
| 角色 | 定义按位与运算符 & |
| 签名 | __and__(self, other) -> object |
| 返回值 | 新对象,或 NotImplemented |
| 调用时机 | x & y,以及反向尝试 |
| 底层 | C 层的 nb_and 槽位 |
与 __rand__ 的关系 |
反向按位与,用于 other & self,可复用正向逻辑 |
与 __iand__ 的关系 |
就地按位与 &=,通常修改自身并返回 self |
| 最佳实践 | 返回新对象、类型检查、使用 NotImplemented、实现反向和就地方法 |
掌握 __and__ 可以实现自定义类对按位与运算的支持,无论是数学上的位运算还是具有特殊语义的交集操作。通过理解其底层机制和设计原则,你可以构建出与 Python 内置类型一样自然、健壮的类。
如果在学习过程中遇到问题,欢迎在评论区留言讨论!