Python 魔法方法:让自定义对象更 Pythonic

1. 引言:什么是魔法方法

在 Python 中,以双下划线开头和结尾的方法(如 __init__、__str__)被称为魔法方法 (Magic Methods),也叫双下方法(Dunder Methods,Double Underscore 的缩写)。

它们不是给开发者直接调用的,而是由 Python 解释器在特定场景下自动触发。比如:

  • 用 len(obj) 时,解释器会去找 obj.__len__();
  • 用 obj[key] 时,会去找 obj.__getitem__(key);
  • 用 a + b 时,会去找 a.__add__(b)。

魔法方法的价值在于:让你的自定义类像内置类型一样自然地融入 Python 的语法体系 。一个设计良好的类,使用者不需要记住一堆 get_xxx()、set_xxx() 方法,而是可以直接用 len()、for 循环、==、+ 等原生语法操作它------这就是所谓的 Pythonic。

本文将从最常用的几个魔法方法讲起,逐步深入到运算符重载,最后手写一个完整的自定义集合类,把前面所有知识点串起来。

下面这张图概括了本文涉及的魔法方法分类,方便你建立整体认知:

flowchart TD A["Python 魔法方法"] --> B["对象生命周期与表示"] A --> C["容器协议"] A --> D["相等性与哈希"] A --> E["运算符重载"] B --> B1["__init__ 初始化"] B --> B2["__str__ 面向用户"] B --> B3["__repr__ 面向开发者"] C --> C1["__len__ 支持 len()"] C --> C2["__getitem__ 下标与迭代"] D --> D1["__eq__ 相等语义"] D --> D2["__hash__ 可哈希"] E --> E1["算术运算符 __add__ 等"] E --> E2["比较运算符 __lt__ 等"] E --> E3["反向与就地运算符 __radd__ / __iadd__"]

2. 对象的生命周期与表示:__init__、__str__、__repr__

2.1 __init__:初始化对象

__init__ 是最常见的魔法方法,在对象创建后负责初始化实例属性。

python 复制代码
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


p = Point(1, 2)
print(p.x, p.y)  # 1 2

需要注意 __init__ 与 __new__ 的区别:__new__ 负责创建 实例(返回实例对象),__init__ 负责初始化 实例(返回 None)。绝大多数场景只需要重写 __init__。

对象从创建到初始化的完整流程如下:

flowchart LR A["Point(1, 2)"] --> B["__new__ 创建实例"] B --> C["返回实例对象"] C --> D["__init__ 初始化属性"] D --> E["返回 None"] E --> F["可用对象 p"]

2.2 __str__:面向用户的字符串表示

__str__ 在调用 str(obj) 或 print(obj) 时被触发,目标是给人类阅读,应当简洁、可读。

python 复制代码
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"({self.x}, {self.y})"


print(Point(1, 2))  # (1, 2)

2.3 __repr__:面向开发者的字符串表示

__repr__ 在调用 repr(obj)、在交互式解释器中直接输入对象、以及对象被放进容器(如列表)打印时被触发。它的目标是尽可能精确地还原对象 ,理想情况下 eval(repr(obj)) 能重建一个等价对象。

python 复制代码
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"


p = Point(1, 2)
print(repr(p))   # Point(1, 2)
print([p, p])    # [Point(1, 2), Point(1, 2)]

2.4 最佳实践:只写 __repr__ 就够了

如果只定义了 __repr__ 而没有定义 __str__,那么 str() 会自动回退到 __repr__。因此一个常见的做法是:优先实现 __repr__,需要更友好的展示时再补充 __str__。

python 复制代码
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"

    def __str__(self):
        return f"({self.x}, {self.y})"


p = Point(1, 2)
print(str(p))    # (1, 2)
print(repr(p))   # Point(1, 2)

3. 容器协议:__len__ 与 __getitem__

3.1 __len__:支持 len()

实现 __len__ 后,对象就能被 len() 调用,同时也会影响布尔判断(bool(obj) 在 __len__ 返回 0 时为 False)。

python 复制代码
class Playlist:
    def __init__(self, songs):
        self.songs = list(songs)

    def __len__(self):
        return len(self.songs)


pl = Playlist(["A", "B", "C"])
print(len(pl))        # 3
print(bool(Playlist([])))  # False

3.2 __getitem__:支持下标访问与迭代

实现 __getitem__ 后,对象支持 obj[index]。更妙的是,只要实现了 __getitem__,对象就自动支持 for 循环迭代 ------解释器会从索引 0 开始不断调用,直到抛出 IndexError 为止。

python 复制代码
class Playlist:
    def __init__(self, songs):
        self.songs = list(songs)

    def __len__(self):
        return len(self.songs)

    def __getitem__(self, index):
        return self.songs[index]


pl = Playlist(["A", "B", "C"])
print(pl[0])        # A
print(pl[-1])       # C
print(pl[0:2])      # ['A', 'B'],切片也能工作

for song in pl:
    print(song)

由于 self.songs 本身是列表,把索引直接透传给它,切片、负索引等能力就都免费获得了。

4. 相等性与哈希:__eq__ 与 __hash__

4.1 __eq__:定义相等语义

默认情况下,== 比较的是对象身份(即 is)。重写 __eq__ 可以让两个内容相同的对象判定为相等。

python 复制代码
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y


print(Point(1, 2) == Point(1, 2))  # True

这里返回 NotImplemented 而不是 False,是为了让 Python 有机会尝试对方的 __eq__,是更规范的做法。

4.2 __hash__:让对象可哈希

一旦重写了 __eq__,Python 会自动把 __hash__ 置为 None,对象就不可哈希 了,也就无法放进 set 或作为 dict 的键。

如果对象是不可变 的,应当同时实现 __hash__,且保证:两个相等对象的哈希值必须相同。

是否实现 __hash__,可以按下面的决策路径判断:

flowchart TD A["重写了 __eq__"] --> B{"对象是否可变"} B -->|"不可变"| C["实现 __hash__"] C --> D["保证相等对象哈希值相同"] D --> E["可放入 set / 作为 dict 键"] B -->|"可变"| F["不要实现 __hash__"] F --> G["保持不可哈希,避免破坏容器结构"]
python 复制代码
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))


print(hash(Point(1, 2)) == hash(Point(1, 2)))  # True
print(len({Point(1, 2), Point(1, 2)}))         # 1

注意:如果对象是可变 的(属性会变化),就不要实现 __hash__,否则哈希值变化会破坏 set/dict 的内部结构。

5. 运算符重载

运算符重载让自定义对象支持 +、-、*、< 等运算,本质上是实现对应的魔法方法。

5.1 算术运算符

运算符 魔法方法 说明
+ __add__ 加法
- __sub__ 减法
* __mul__ 乘法
/ __truediv__ 真除法
% __mod__ 取模
python 复制代码
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x!r}, {self.y!r})"


print(Vector(1, 2) + Vector(3, 4))  # Vector(4, 6)

5.2 比较运算符

运算符 魔法方法
< __lt__
<= __le__
> __gt__
>= __ge__

借助 functools.total_ordering 装饰器,只需实现 __eq__ 和其中一个比较方法,其余会自动补齐。

python 复制代码
from functools import total_ordering


@total_ordering
class Version:
    def __init__(self, major, minor):
        self.major = major
        self.minor = minor

    def _key(self):
        return (self.major, self.minor)

    def __eq__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return self._key() == other._key()

    def __lt__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return self._key() < other._key()

    def __repr__(self):
        return f"Version({self.major}, {self.minor})"


print(Version(1, 2) < Version(1, 10))   # True
print(Version(2, 0) >= Version(1, 9))   # True

5.3 反向运算符与就地运算符

  • 反向运算符 :当左操作数不支持该运算时,Python 会尝试右操作数的 __radd__ 等方法。例如 1 + vector 会调用 vector.__radd__(1)。
  • 就地运算符 :+= 对应 __iadd__,若未实现则回退到 __add__ 并重新赋值。
python 复制代码
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        if isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        return NotImplemented

    def __radd__(self, other):
        return self.__add__(other)

    def __repr__(self):
        return f"Vector({self.x!r}, {self.y!r})"


print(Vector(1, 2) + 10)  # Vector(11, 12)
print(10 + Vector(1, 2))  # Vector(11, 12)

6. 实战:实现一个自定义集合类

下面把前面学到的魔法方法综合起来,实现一个轻量的 SortedSet:内部用列表存储,保证元素唯一且有序,并支持 len()、下标访问、迭代、相等比较、哈希以及集合运算。

python 复制代码
from bisect import bisect_left, insort


class SortedSet:
    def __init__(self, iterable=()):
        self._items = []
        for item in iterable:
            self.add(item)

    # ---------- 容器协议 ----------
    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __contains__(self, item):
        i = bisect_left(self._items, item)
        return i < len(self._items) and self._items[i] == item

    def __iter__(self):
        return iter(self._items)

    # ---------- 表示 ----------
    def __repr__(self):
        return f"SortedSet({self._items!r})"

    # ---------- 相等与哈希 ----------
    def __eq__(self, other):
        if not isinstance(other, SortedSet):
            return NotImplemented
        return self._items == other._items

    def __hash__(self):
        return hash(tuple(self._items))

    # ---------- 运算符重载 ----------
    def __or__(self, other):
        """并集:a | b"""
        if not isinstance(other, SortedSet):
            return NotImplemented
        return SortedSet(self._items + other._items)

    def __and__(self, other):
        """交集:a & b"""
        if not isinstance(other, SortedSet):
            return NotImplemented
        return SortedSet(x for x in self._items if x in other)

    def __sub__(self, other):
        """差集:a - b"""
        if not isinstance(other, SortedSet):
            return NotImplemented
        return SortedSet(x for x in self._items if x not in other)

    # ---------- 业务方法 ----------
    def add(self, item):
        i = bisect_left(self._items, item)
        if i == len(self._items) or self._items[i] != item:
            insort(self._items, item)

    def discard(self, item):
        i = bisect_left(self._items, item)
        if i < len(self._items) and self._items[i] == item:
            self._items.pop(i)

使用效果:

python 复制代码
a = SortedSet([3, 1, 2, 2])
b = SortedSet([2, 4])

print(a)              # SortedSet([1, 2, 3])
print(len(a))         # 3
print(a[0])           # 1
print(2 in a)         # True
print(a | b)          # SortedSet([1, 2, 3, 4])
print(a & b)          # SortedSet([2])
print(a - b)          # SortedSet([1, 3])
print(a == SortedSet([1, 2, 3]))  # True

for x in a:
    print(x)          # 1 2 3

这个类虽然简单,但已经具备了一个「像内置类型一样好用」的容器应有的核心能力:可以用 len()、下标、in、for、==、|、&、- 等原生语法操作它。

SortedSet 的内部结构与对外能力可以这样理解:

flowchart TD A[&#34;SortedSet&#34;] --> B[&#34;内部 _items 有序列表&#34;] A --> C[&#34;容器协议&#34;] A --> D[&#34;表示与比较&#34;] A --> E[&#34;集合运算&#34;] C --> C1[&#34;__len__ / __getitem__&#34;] C --> C2[&#34;__contains__ / __iter__&#34;] D --> D1[&#34;__repr__&#34;] D --> D2[&#34;__eq__ / __hash__&#34;] E --> E1[&#34;__or__ 并集&#34;] E --> E2[&#34;__and__ 交集&#34;] E --> E3[&#34;__sub__ 差集&#34;] B --> F[&#34;add / discard 维护有序唯一&#34;]

7. 常见陷阱与最佳实践

  1. __eq__ 与 __hash__ 要成对考虑 :重写 __eq__ 后 __hash__ 会被置为 None,不可变对象记得补上 __hash__,可变对象则不要实现。
  2. 返回 NotImplemented 而非 False :在比较和运算方法中,遇到不支持的类型应返回 NotImplemented,让 Python 尝试反向操作。
  3. __repr__ 优先于 __str__ :__repr__ 是调试和日志的主力,务必实现;__str__ 只在需要更友好的展示时补充。
  4. 不要滥用魔法方法:只实现真正需要的协议,过度重载会让代码难以理解。
  5. 保持语义一致 :__len__ 返回非负整数,__getitem__ 越界抛 IndexError,__eq__ 满足自反、对称、传递------遵循内置类型的约定,使用者才不会踩坑。

8. 总结

魔法方法是 Python 数据模型的基石。掌握它们,你就能写出与内置类型无缝协作的类:

  • __init__ / __str__ / __repr__ 负责对象的创建与表示;
  • __len__ / __getitem__ 让对象支持容器语法;
  • __eq__ / __hash__ 定义相等与哈希语义;
  • 运算符重载让对象支持 +、<、| 等运算;
  • 把这些组合起来,就能实现一个真正 Pythonic 的自定义集合类。

下次写类时,不妨先问自己一句:这个对象,能不能用更 Python 的方式被使用?

相关推荐
雨晨源码(同名B站)1 小时前
深度学习毕业设计选题推荐:基于YOLO11的交通标志检测系统设计与实现(Python+Django+Vue3+对接deepseek)
人工智能·python·深度学习·机器学习·django·课程设计
2601_967979122 小时前
python中str函数怎么用?使用方法与注意事项详解
python·使用方法·字符串转换·注意事项·str函数
青少儿编程课堂2 小时前
蓝桥杯青少组图形化+信息素养+电子学会Scratch考级:校园口算闯关解析
c++·python·算法·bfs·信息学竞赛
fanruitian2 小时前
python 机器学习 交叉验证train_test_split 网格搜索GridSearchCV
开发语言·python·机器学习
前端精髓11 小时前
结构化输出with_structured_output(抽卡)
python·langchain
尹人入圣12 小时前
目前热门的IP驱动产业新场景新工具
网络·python·网络协议·tcp/ip
Nuanyt13 小时前
Agent 学习笔记(下篇三):上线工程、进阶选择与操作界面(Stage 7 / 7.5 / 8 + 延伸路线)
笔记·python·学习·memory·aiagent·harness
吴卫斌13 小时前
定投ETF实战指南(四):行业/主题ETF——定投的“进攻端“
开发语言·python·股票·量化
2601_9679873713 小时前
Python——入门级(定义功能def函数)
python·函数·可变参数·默认参数·关键字参数