小白学 Python
AI 编程指南GitHub
© 2026 小白学 Python · 基于 walter201230/Python 教程
课程目录关于本站联系方式隐私政策GitHub

魔法方法

完整讲解 · 5 段教学·配 5 道练习题·预计 35 分钟

本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。

教学 01 / 05

第一节:魔法方法

在 Python 中,所有以双下划线 __ 包起来的方法,都统称为 魔法方法(Magic Methods),也叫 魔术方法 / dunder 方法(dunder = double underscore)。

最常见的就是我们在上一章见过的 __init__。它在创建对象时自动被调用——你从来没有手动写过 obj.__init__(...),但它就是会跑。

魔法方法的能力远不止 __init__。它能让你自定义 Python 内置语法对你的对象的行为,比如:

写法背后调用的魔法方法
print(obj) / str(obj)__str__
obj1 == obj2__eq__
len(obj)__len__
obj[0]__getitem__
obj1 + obj2__add__

一句话:魔法方法 = 把你自己定义的类,接入 Python 内置语法。

列出一个类的魔法方法

任意一个类(哪怕一个空类)都已经从 object 继承了一堆魔法方法。用 dir() 可以查看:

python到闯关页运行这段 →
class User:
    pass

print(dir(User()))

输出(截取一部分):

['__class__', '__delattr__', '__dict__', '__dir__', '__eq__', '__format__',
 '__getattribute__', '__hash__', '__init__', '__lt__', '__new__', '__repr__',
 '__setattr__', '__str__', ...]

可以看到,魔法方法非常多。本节我们挑最常用的几个学透——__str__ / __repr__、__eq__、__len__ / __getitem__、__add__。学完这几个,你写出的类就有了「Python 风味」。

教学 02 / 05

二、__str__ 与 __repr__:让 print 输出友好

先看一个问题。我们写一个最普通的 Book 类:

python到闯关页运行这段 →
class Book:
    def __init__(self, title):
        self.title = title

b = Book('Python 入门')
print(b)

输出是什么?

<__main__.Book object at 0x10a9f3d50>

一串 ✗ 的乱码。这是因为默认情况下,print(obj) 会调用 object 的 __str__,它只会告诉你「这是一个 Book 对象,地址是 0x...」——对人类来说毫无信息量。

1、__str__:给「人」看的字符串

我们来重写它:

python到闯关页运行这段 →
class Book:
    def __init__(self, title):
        self.title = title

    def __str__(self):
        return f'《{self.title}》'

b = Book('Python 入门')
print(b)         # 《Python 入门》
print(str(b))    # 《Python 入门》

__str__ 必须返回一个字符串。print(obj) 和 str(obj) 都会调用它。

2、__repr__:给「程序员」看的字符串

__repr__ 也是返回字符串,但用途不同:它通常是面向开发者的、最好能复现这个对象的写法。

最典型的场景是:在交互式终端里直接敲变量名,或者把对象放进 list 后 print:

python到闯关页运行这段 →
class Book:
    def __init__(self, title):
        self.title = title

    def __str__(self):
        return f'《{self.title}》'

    def __repr__(self):
        return f"Book(title={self.title!r})"

b = Book('Python 入门')
print(b)        # 《Python 入门》  ← 走 __str__
print([b])      # [Book(title='Python 入门')]  ← 容器里走 __repr__

注意:

  • 如果只定义了 __repr__、没定义 __str__,那 print(obj) 会回退到 __repr__
  • 反过来不会——只定义 __str__ 时,[obj] 仍然显示默认的乱码

经验法则:至少先把 __repr__ 写了。再考虑要不要加更友好的 __str__。

练习 1 / 5用 __str__ 让 print 输出友好去闯关页做这题 →
教学 03 / 05

三、__eq__:自定义「相等」的含义

默认情况下,== 比较两个对象是不是同一个对象(同一块内存),等价于 is:

python到闯关页运行这段 →
class Money:
    def __init__(self, amount):
        self.amount = amount

a = Money(100)
b = Money(100)
print(a == b)  # False  ← 两个不同对象,默认就是不相等

但从「业务」角度看,100 块钱就是 100 块钱——只要金额相同,就该相等。这时候定义 __eq__:

python到闯关页运行这段 →
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __eq__(self, other):
        return self.amount == other.amount

a = Money(100)
b = Money(100)
c = Money(50)

print(a == b)  # True
print(a == c)  # False
print(a != c)  # True   ← != 默认是 __eq__ 的反

__eq__(self, other) 接收两个参数,返回 True / False。Python 会把 a == b 翻译成 a.__eq__(b)。

不过上面这个版本有个坑:other 不一定是 Money。要是有人写 Money(100) == 100,other 就是数字 100,没有 .amount 属性,直接抛 AttributeError 报错——比较运算不该报错,应该老老实实返回 False。

所以稳妥的写法是先检查类型再比较:

python到闯关页运行这段 →
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __eq__(self, other):
        return isinstance(other, Money) and self.amount == other.amount

print(Money(100) == 100)  # False——不再报错

isinstance(other, Money) 判断 other 是不是 Money 类型(含子类);不是就直接短路返回 False,后半句根本不会执行。

完整的比较运算符家族

写法魔法方法
a == b__eq__
a != b__ne__(不写也会自动用 __eq__ 取反)
a < b__lt__
a > b__gt__
a <= b__le__
a >= b__ge__

举个完整例子:

python到闯关页运行这段 →
class Number:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

    def __lt__(self, other):
        return self.value < other.value

n1 = Number(2)
n2 = Number(3)
print(n1 == n2)  # False
print(n1 < n2)   # True

(真实代码里这些方法同样建议先做 isinstance 检查,这里为了突出运算符家族省略了。)

__hash__ 的小坑

只要你重写了 __eq__,Python 会自动把 __hash__ 设为 None——也就是说,这个对象不能再放进 set / dict 的 key。

如果想让对象既可比较又可哈希,要手动加上 __hash__:

python到闯关页运行这段 →
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __eq__(self, other):
        return isinstance(other, Money) and self.amount == other.amount

    def __hash__(self):
        return hash(self.amount)

规则:相等的对象,hash 必须相等。所以 __hash__ 通常基于 __eq__ 用到的同一组属性。

练习 2 / 5用 __eq__ 自定义相等去闯关页做这题 →
教学 04 / 05

四、__len__ 与 __getitem__:让对象「像容器」

Python 的 list、tuple、str、dict 都是「容器」。它们都支持:

  • len(x) 取长度
  • x[0] 按 key 取元素
  • for item in x 迭代

如果你自己写的类也想支持这些语法呢?——只要实现对应的魔法方法即可。

1、__len__:让 len() 工作

python到闯关页运行这段 →
class Playlist:
    def __init__(self, songs):
        self.songs = songs

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

p = Playlist(['歌1', '歌2', '歌3'])
print(len(p))   # 3

__len__ 必须返回一个非负整数。如果不实现,len(p) 会抛 TypeError: object of type 'Playlist' has no len()。

2、__getitem__:让 obj[i] 工作

python到闯关页运行这段 →
class Playlist:
    def __init__(self, songs):
        self.songs = songs

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

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

p = Playlist(['歌1', '歌2', '歌3'])
print(p[0])   # 歌1
print(p[-1])  # 歌3

__getitem__(self, key) 中的 key 可以是:

  • 整数(普通索引)
  • 切片对象 slice(1, 3)(来自 p[1:3] 这种写法)
  • 任意 hashable 值(如果你想模仿 dict)

3、白送的 for 迭代

只要你实现了 __getitem__,不用再写 __iter__,for 循环也能用了:

python到闯关页运行这段 →
class Playlist:
    def __init__(self, songs):
        self.songs = songs

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

p = Playlist(['歌1', '歌2', '歌3'])
for s in p:
    print(s)
# 歌1
# 歌2
# 歌3

Python 会从 p[0]、p[1]、p[2]... 一直叫,直到抛 IndexError 才停。

4、可写容器:__setitem__

python到闯关页运行这段 →
class Playlist:
    def __init__(self, songs):
        self.songs = songs

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

    def __setitem__(self, i, value):
        self.songs[i] = value

p = Playlist(['a', 'b', 'c'])
p[0] = 'A'        # 触发 __setitem__
print(p.songs)    # ['A', 'b', 'c']

总结:实现 __len__ + __getitem__ 就有「只读容器」;再加 __setitem__ / __delitem__ 就成「可变容器」。

练习 3 / 5用 __len__ 让 len() 工作去闯关页做这题 →练习 4 / 5用 __getitem__ 让对象支持下标去闯关页做这题 →
教学 05 / 05

五、__add__:让对象支持 + 运算

Python 的 + 对不同类型有不同含义:

  • 1 + 2 → 3(数字相加)
  • 'a' + 'b' → 'ab'(字符串拼接)
  • [1] + [2] → [1, 2](列表拼接)

这些都不是 Python「写死」的——它们各自的类都实现了 __add__。我们也可以让自己的类支持 +。

用 __add__ 实现二维向量加法

python到闯关页运行这段 →
class Vec2:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vec2(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f'Vec2({self.x}, {self.y})'

v1 = Vec2(1, 2)
v2 = Vec2(3, 4)
v3 = v1 + v2     # 触发 v1.__add__(v2)
print(v3)        # Vec2(4, 6)

注意:__add__ 通常返回一个新对象,而不是修改 self。这跟 1 + 2 不会改变 1 是同一个道理。

完整的算术运算符家族

写法魔法方法
a + b__add__
a - b__sub__
a * b__mul__
a / b__truediv__
a // b__floordiv__
a % b__mod__
a ** b__pow__
-a__neg__
abs(a)__abs__

还有更多

魔法方法非常多,本节只是入门最常用的几个。下面这些等你日后用到再查就行:

魔法方法触发场景
__call__让对象像函数一样被调用:obj()
__iter__ / __next__自定义迭代器
__enter__ / __exit__with 语句的上下文管理器
__getattr__ / __setattr__拦截属性访问
__new__控制对象创建(在 __init__ 之前)

本节小结

  • 魔法方法是 __name__ 形式、由 Python 自动调用的特殊方法
  • __str__ / __repr__:定义对象怎么变成字符串
  • __eq__ 等:定义比较运算
  • __len__ / __getitem__:让对象像容器
  • __add__ 等:让对象支持算术运算

下一步去做练习,把这几个魔法方法亲手写一遍。

练习 5 / 5用 __add__ 让对象支持 +去闯关页做这题 →

读完了?动手练一遍才算真会。

去闯关模式练习 →