Python 3.12 MagicMethods - 56 - __and__

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 时,解释器会:

  1. 获取 x 的类型对象的 tp_as_number 结构。
  2. 如果存在 nb_and,则调用它,传入 xy,返回结果对象或 Py_NotImplemented
  3. 如果 xnb_and 返回 Py_NotImplemented,则尝试获取 y 的类型对象的 nb_and,并调用它,但此时参数顺序已交换(即调用 y__rand__ 对应的 C 函数)。
  4. 如果仍然失败,则抛出 TypeError

对于 Python 层定义的 __and__,它会被包装到 nb_and 槽位中。反向方法 __rand__ 也会在必要时被调用,用于处理左操作数不支持该运算的情况。


4. 设计原则与最佳实践

  • 遵循数学/语义一致性 :对于整数类型,按位与应满足 a & b 的每一位为 1 当且仅当 ab 的对应位均为 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 处理同类型 如果 otherMyInt,使用内置 & 计算,返回新 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__selfMyIntotherint),而我们的 __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 内置类型一样自然、健壮的类。

如果在学习过程中遇到问题,欢迎在评论区留言讨论!

相关推荐
1candobetter2 小时前
JAVA后端开发——如何在多层代理环境下实现稳定的签名算法:Host 与端口问题解析
java·开发语言
爱敲代码的菜菜2 小时前
【项目】基于正倒排索引的Java文档搜索引擎
java·开发语言·前端·javascript·搜索引擎·servlet
帐篷Li2 小时前
【BBF系列协议】USP/TR-369 Agent 开发计划
开发语言·python
m0_528174452 小时前
用Python读取和处理NASA公开API数据
jvm·数据库·python
重庆小透明2 小时前
【java基础内容】ConcurrentHashmap源码万字解析
java·开发语言
Yupureki2 小时前
《MySQL数据库基础》4. 数据类型
c语言·开发语言·数据结构·数据库·c++·mysql
C++ 老炮儿的技术栈2 小时前
C++、C#常用语法对比
c语言·开发语言·c++·qt·c#·visual studio
共享家95272 小时前
Java入门(继承)
java·开发语言
Bert.Cai2 小时前
Python默认参数详解
开发语言·python