Python 魔术方法入门

Python 魔术方法入门

目录

  • 第一部分:先补齐两个必要概念
    • [1.1 什么是"类"和"对象"](#1.1 什么是"类"和"对象" "#11-%E4%BB%80%E4%B9%88%E6%98%AF%E7%B1%BB%E5%92%8C%E5%AF%B9%E8%B1%A1")
    • [1.2 self 到底是什么](#1.2 self 到底是什么 "#12-self-%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88")
    • [1.3 方法:写在类里面的函数](#1.3 方法:写在类里面的函数 "#13-%E6%96%B9%E6%B3%95%E5%86%99%E5%9C%A8%E7%B1%BB%E9%87%8C%E9%9D%A2%E7%9A%84%E5%87%BD%E6%95%B0")
  • 第二部分:魔术方法是什么
    • [2.1 一个生活化类比:门与自动门](#2.1 一个生活化类比:门与自动门 "#21-%E4%B8%80%E4%B8%AA%E7%94%9F%E6%B4%BB%E5%8C%96%E7%B1%BB%E6%AF%94%E9%97%A8%E4%B8%8E%E8%87%AA%E5%8A%A8%E9%97%A8")
    • [2.2 为什么名字要带双下划线](#2.2 为什么名字要带双下划线 "#22-%E4%B8%BA%E4%BB%80%E4%B9%88%E5%90%8D%E5%AD%97%E8%A6%81%E5%B8%A6%E5%8F%8C%E4%B8%8B%E5%88%92%E7%BA%BF")
    • [2.3 它与普通方法的三个区别](#2.3 它与普通方法的三个区别 "#23-%E5%AE%83%E4%B8%8E%E6%99%AE%E9%80%9A%E6%96%B9%E6%B3%95%E7%9A%84%E4%B8%89%E4%B8%AA%E5%8C%BA%E5%88%AB")
    • [2.4 什么情况下会被自动调用](#2.4 什么情况下会被自动调用 "#24-%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%BC%9A%E8%A2%AB%E8%87%AA%E5%8A%A8%E8%B0%83%E7%94%A8")
    • [2.5 一个可以自己验证的方法](#2.5 一个可以自己验证的方法 "#25-%E4%B8%80%E4%B8%AA%E5%8F%AF%E4%BB%A5%E8%87%AA%E5%B7%B1%E9%AA%8C%E8%AF%81%E7%9A%84%E6%96%B9%E6%B3%95")
  • 第三部分:逐个学会常用魔术方法
    • [3.1 __init__:给新对象填初始信息](#3.1 init:给新对象填初始信息 "#31-init%E7%BB%99%E6%96%B0%E5%AF%B9%E8%B1%A1%E5%A1%AB%E5%88%9D%E5%A7%8B%E4%BF%A1%E6%81%AF")
    • [3.2 __str__:面向用户的文字描述](#3.2 str:面向用户的文字描述 "#32-str%E9%9D%A2%E5%90%91%E7%94%A8%E6%88%B7%E7%9A%84%E6%96%87%E5%AD%97%E6%8F%8F%E8%BF%B0")
    • [3.3 __repr__:面向程序员的文字描述](#3.3 repr:面向程序员的文字描述 "#33-repr%E9%9D%A2%E5%90%91%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E6%96%87%E5%AD%97%E6%8F%8F%E8%BF%B0")
    • [3.4 __len__:让对象有"长度"](#3.4 len:让对象有"长度" "#34-len%E8%AE%A9%E5%AF%B9%E8%B1%A1%E6%9C%89%E9%95%BF%E5%BA%A6")
    • [3.5 __getitem__:让对象能用 [] 取值](#3.5 getitem:让对象能用 [] 取值 "#35-getitem%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E7%94%A8-%E5%8F%96%E5%80%BC")
    • [3.6 __call__:让对象能像函数一样被调用](#3.6 call:让对象能像函数一样被调用 "#36-call%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E5%83%8F%E5%87%BD%E6%95%B0%E4%B8%80%E6%A0%B7%E8%A2%AB%E8%B0%83%E7%94%A8")
    • [3.7 __add__:让对象能用 +](#3.7 add:让对象能用 + "#37-add%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E7%94%A8-")
    • [3.8 __eq__:让对象能用 == 比较](#3.8 eq:让对象能用 == 比较 "#38-eq%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E7%94%A8-%E6%AF%94%E8%BE%83")
    • [3.9 __del__:对象被回收时的收尾](#3.9 del:对象被回收时的收尾 "#39-del%E5%AF%B9%E8%B1%A1%E8%A2%AB%E5%9B%9E%E6%94%B6%E6%97%B6%E7%9A%84%E6%94%B6%E5%B0%BE")
    • [3.10 综合示例:把学到的串起来](#3.10 综合示例:把学到的串起来 "#310-%E7%BB%BC%E5%90%88%E7%A4%BA%E4%BE%8B%E6%8A%8A%E5%AD%A6%E5%88%B0%E7%9A%84%E4%B8%B2%E8%B5%B7%E6%9D%A5")
  • 第四部分:速查表
  • 第五部分:给初学者的五条建议
  • 附录:如何运行本文代码

第一部分:先补齐两个必要概念

要理解魔术方法,只需要两个概念。这一部分刻意不涉及继承、装饰器、元类等进阶内容。

1.1 什么是"类"和"对象"

是一张图纸对象 是按照图纸造出来的实物

生活里到处是这个结构:

图纸(类) 实物(对象)
房子的设计图 你住的那套房
月饼模具 一块块的月饼
宠物狗的品种描述 你家的旺财

写代码时,"图纸"用 class 定义:

python 复制代码
class Dog:              # 定义一个类,名叫 Dog
    pass                # 先留空,后面再往里加东西

有了图纸,就能造实物。造实物的写法是类名后面加括号

python 复制代码
d = Dog()               # 造出一个 Dog 对象,赋值给 d

到这里,d 就是一个真实存在的对象了,虽然它现在还什么都不会。

一句话记住:类名加括号,就得到一个对象。

1.2 self 到底是什么

这是初学者第一个坎。

self 表示"当前这个对象自己"。

看这个例子:

python 复制代码
class Dog:
    def set_name(self, name):
        self.name = name        # 把 name 存进"我自己"身上

d1 = Dog()
d2 = Dog()
d1.set_name("旺财")             # 此时 self 就是 d1
d2.set_name("小黑")             # 此时 self 就是 d2

d1.set_name("旺财") 这个写法,Python 实际理解成 Dog.set_name(d1, "旺财")------d1 被自动填进了 self 的位置

所以:

  • self 不需要你手动传,Python 自动传。
  • 写方法时,self 必须是第一个参数
  • self.name = name 的意思是"在这个对象身上存一个叫 name 的东西"。

存进去之后,用 对象.名字 就能取出来:

python 复制代码
print(d1.name)      # 旺财
print(d2.name)      # 小黑

为什么两个对象互不干扰? 因为每个对象都有自己的一小块存储空间。可以用 __dict__ 看到里面装了什么:

python 复制代码
print(d1.__dict__)  # {'name': '旺财'}

实测确认:Dog("旺财") 之后,d.__dict__{'name': '旺财'}------信息确实存在对象自己身上。

1.3 方法:写在类里面的函数

同一段逻辑,写在外面叫函数 ,写在类里面叫方法

python 复制代码
def bark():             # 函数:外面
    print("汪汪")

class Dog:
    def bark(self):     # 方法:类里面,第一个参数是 self
        print("汪汪")

调用方式不同:

python 复制代码
bark()              # 函数:直接写名字调用
d = Dog()
d.bark()            # 方法:用"对象.方法名()"调用

唯一实质差别就是多了 self 除此之外,写法和函数完全一样------一样可以有参数、有返回值、有 if 和 for。

记住这句就够了:方法是待在类里面的函数,第一个参数是 self


第二部分:魔术方法是什么

2.1 一个生活化类比:门与自动门

想象一栋楼里的两种门:

普通门 自动门
怎么开 你得手动推 走近它自己开
谁来操作 你自己 门自己感应
你的感受 "我要去推门" "走过去就行了"

普通方法 = 普通门

python 复制代码
d.bark()                # 你得手动写出来,它才会执行

魔术方法 = 自动门

python 复制代码
print(d)                # 你没写 __str__,但它被自动触发了
len(d)                  # 你没写 __len__,但被自动触发了
d1 == d2                # 你没写 __eq__,但被自动触发了

这就是魔术方法的全部秘密:它不是让你手动调用的,而是在你使用某个运算符或内建函数时,Python 自动帮你调用的。

所以叫"魔术"------你只做了一个很自然的动作(printlen+),对象却神奇地做出了反应。

2.2 为什么名字要带双下划线

用生活场景理解:

假设一个小区里有两种信箱------

  • 普通信箱,写着住户名字:张三
  • 系统信箱,统一刷成特殊颜色、写着 __物业__只有物业才能往里投东西

双下划线就是那个"特殊颜色标记"。它告诉 Python:这个名字是系统保留的,不是你随便起的。

如果你起的名字和系统保留名撞了,Python 会按系统规则来解释你的代码,行为很可能和你想的不一样。所以:

命名形式 含义 你该怎么做
name 普通名字 随便起
_name "我打算内部使用" 一种礼貌约定,没有强制力
__name 类内私有 会被 Python 改名,日常少用
__name__ 系统保留 只使用 Python 已定义的,不要自己造

最后一行是最重要的:看到 __x__ 这种形式,先认为"这是 Python 已经规定好的",去查它的含义,而不是自己发明一个。

2.3 它与普通方法的三个区别

对比项 普通方法 魔术方法
谁来调用 你亲手写 对象.方法名() Python 在你用运算符 / 内建函数时自动调用
名字能不能自己起 能,随便起 不能,名字由 Python 规定
要不要记住名字 要,不然你调不到 不用刻意背,用到哪个学哪个

第三点对初学者很重要:你不需要一次性背完几十个魔术方法。常用的就十来个,用到再学,本文覆盖的就是这些。

2.4 什么情况下会被自动调用

规律很简单:只要你的代码里出现了某个运算符或内建函数,Python 就会去找对应的魔术方法。

对应关系如下(左边是你写的,右边是 Python 悄悄调的):

你写的代码 Python 自动调用
print(对象) __str__
len(对象) __len__
对象[0] __getitem__
对象() __call__
对象a + 对象b __add__
对象a == 对象b __eq__
对象 = 类名(...) __init__
对象被删掉时 __del__

有一件事可能会让你意外:你其实早就在用魔术方法了,只是没意识到。

python 复制代码
len([3, 1, 2])      # 3         <- 调用的是 list 的 __len__
[3, 1, 2][0]        # 3         <- 调用的是 list 的 __getitem__
"a" + "b"           # 'ab'      <- 调用的是 str 的 __add__
3 == 3.0            # True      <- 调用的是 int 的 __eq__
str(123)            # '123'     <- 调用的是 int 的 __str__

以上五条均为实测输出。列表、字符串、数字这些内建类型,全都实现了一套魔术方法 ------你用 + 能拼字符串,正是 str.__add__ 在工作。

学魔术方法,本质上是让自己的类也具备内建类型那样的能力

2.5 一个可以自己验证的方法

想知道一个魔术方法有没有被调用?在里面加一句 print 就行了:

python 复制代码
class Dog:
    def __str__(self):
        print("    [__str__ 被调用了]")     # 加这一句观察
        return "一只狗"

print(Dog())

输出:

text 复制代码
    [__str__ 被调用了]
一只狗

以后每学一个魔术方法,都可以用这招确认它到底有没有触发、什么时候触发。本文后面所有"运行结果"都是这样实测出来的。


第三部分:逐个学会常用魔术方法

以下 9 个魔术方法,每个都按统一结构展开:

作用何时被自动触发极简代码示例运行结果初学者易错点

3.1 __init__:给新对象填初始信息

作用

创建对象时,自动把初始信息存进对象里。可以理解成"对象出生时的登记手续"。

何时被自动触发

对象 = 类名(...) 的时候,括号里传的东西会原样交给 __init__

极简代码示例

python 复制代码
class Dog:
    def __init__(self, name):      # 创建对象时自动执行
        self.name = name           # 把名字存进对象里

    def __str__(self):             # 被 print() 时自动执行
        return f"一只叫 {self.name} 的狗"

d = Dog("旺财")                    # 括号里的 "旺财" 传给了 name
print(d)

运行结果

text 复制代码
一只叫 旺财 的狗

初学者易错点

易错点 1:把 __init__ 写成 __int___init_

少了或多了下划线,它就不是魔术方法了,创建对象时不会报错,但也不会执行。写错后如果发现"我明明写了却没生效",先检查下划线。

易错点 2:忘记写 self

python 复制代码
def __init__(name):         # 少了 self,会报错
    self.name = name

报错信息类似 TypeError: __init__() takes 1 positional argument but 2 were given。原因见 1.2 节------self 是自动传进来的第一个参数,少了它参数数量就错位了。

易错点 3:以为 __init__ 是"创建对象"。

它只是给已经创建好的对象填信息 。真正创建对象的是 __new__,但初学阶段完全不用管它。

易错点 4:在 __init__ 里写 return

__init__ 必须返回 None(也就是不写 return)。写了 return 值 会报 TypeError


3.2 __str__:面向用户的文字描述

作用

决定 print(对象) 时打印出什么。这是给用户看的文字。

何时被自动触发

print(对象)str(对象)、f-string 里插对象时。

极简代码示例

python 复制代码
class Book:
    def __init__(self, title):
        self.title = title

    def __str__(self):
        return f"《{self.title}》"       # 面向用户,写好看点

b = Book("小王子")
print(b)

运行结果

text 复制代码
《小王子》

如果不写 __str__,打印出来会是这样:

text 复制代码
<__main__.Book object at 0x00000180FF303ED0>

这个形式初学者常觉得像乱码,其实它的意思是"这里有个 Book 对象,住在内存的某个地址"。

初学者易错点

易错点 1:忘了 return,写成 print

python 复制代码
def __str__(self):
    print(f"《{self.title}》")       # 错!这里应该是 return

写成 print 会报 TypeError: __str__ returned non-string所有以 __str__ 为首的"返回文字"的魔术方法,都必须用 return

易错点 2:返回的不是字符串。

python 复制代码
def __str__(self):
    return self.title              # 如果 title 是数字就报错

必须是 str 类型。保险做法是用 f-string 包一层:return f"{self.title}"

易错点 3:以为它能管住列表里的显示。

python 复制代码
print(b)            # 《小王子》       ← 走 __str__,符合预期
print([b])          # [<__main__.Book object at 0x...>]   ← 不是想要的效果!

放进列表后显示变丑了。这涉及下一个魔术方法 __repr__,见 3.3。


3.3 __repr__:面向程序员的文字描述

作用

同样是"返回一段文字",但对象不同 ------__repr__给程序员看的,用于调试和列表显示。

何时被自动触发

repr(对象)、在列表或字典里显示对象、以及在交互式命令行里直接输入对象名时。

极简代码示例

python 复制代码
class Book:
    def __init__(self, title):
        self.title = title

    def __repr__(self):
        return f"Book({self.title!r})"      # 面向程序员:能看出怎么造出来

    def __str__(self):
        return f"《{self.title}》"           # 面向用户:好看

b = Book("小王子")
print("print(b)    ->", b)
print("repr(b)     ->", repr(b))
print("列表里显示  ->", [b])

运行结果

text 复制代码
print(b)    -> 《小王子》
repr(b)     -> Book('小王子')
列表里显示  -> [Book('小王子')]

三行的差异值得仔细看 :同一个对象,print 时是给用户看的,repr() 和列表里显示时是给程序员看的。

两者分工速记

__str__ __repr__
给谁看 用户 程序员(调试)
触发方式 print() / str() repr() / 列表内 / 命令行
要求 好看、易懂 精确、能看出对象怎么造出来的
没写会怎样 回退到 __repr__ 无处回退 ,显示 <object at 0x...>

初学者易错点

易错点 1:只写 __str__,然后发现列表里显示很难看。

实测验证:

python 复制代码
class OnlyStr:
    def __str__(self):
        return "我是 OnlyStr"

print(OnlyStr())        # 我是 OnlyStr
print([OnlyStr()])      # [<__main__.OnlyStr object at 0x00000180FF303ED0>]

记住这条规律:容器(列表、字典)内部显示元素时,一律用 __repr__ 所以如果你想让对象在列表里也好看,必须写 __repr__

易错点 2:不确定写哪个,就纠结很久。

实用建议:初学阶段优先写 __repr__ 。因为它同时管住了"调试时看"和"列表里看"两个最常出问题的场景,而 __str__ 只是让 print 更漂亮。想两个都要,就都写。

易错点 3:两个方法写成一模一样。

那不如只写 __repr____str__ 会自动回退到它)。实测验证:只写 __repr__ 的类,print() 时确实会显示 __repr__ 的内容。

易错点 4:不理解结尾的 !r

f"{self.title!r}" 里的 !r 表示"用 repr 的形式插入"。如果 title 是 小王子,插入结果是 '小王子'(带引号);不加 !r 则插入 小王子(不带引号)。带引号更像代码,这对调试更友好。


3.4 __len__:让对象有"长度"

作用

len(对象) 能工作,也能决定对象在 if 里算 True 还是 False

何时被自动触发

len(对象)bool(对象)(当你没写 __bool__ 时)。

极简代码示例

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

    def __len__(self):             # 被 len() 时自动执行
        return len(self.songs)

p = Playlist(["A", "B", "C"])
print(len(p))

运行结果

text 复制代码
3

附带效果:if 判断

实测确认,写了 __len__ 后,对象在 if 里的真假也会跟着变:

text 复制代码
bool(有3首歌的歌单)  -> True
bool(空歌单)         -> False      ← 长度为 0 视为 False

规律:长度为 0 → 假;长度非 0 → 真。 这和列表、字符串的行为一致(bool([])False)。

初学者易错点

易错点 1:返回了负数或非整数。

python 复制代码
def __len__(self):
    return -1          # 报错 ValueError: __len__() should return >= 0

必须返回大于等于 0 的整数

易错点 2:忘了 return

不返回东西会报 TypeError: 'NoneType' object cannot be interpreted as an integer

易错点 3:return len(self.songs) 写成无限递归。

python 复制代码
def __len__(self):
    return len(self)        # 错!len(self) 又去调 __len__,死循环

len 的是里面的那个容器self.songs),不是对象自己。

易错点 4:以为写了 __len__ 就能用 [] 取值。

len()[] 是两个不同的魔术方法。要用 [] 还得写 __getitem__,见下一节。


3.5 __getitem__:让对象能用 [] 取值

作用

让对象支持 对象[下标] 这种写法,效果和列表一样。

何时被自动触发

对象[下标]、切片 对象[1:3]for 遍历对象、in 判断。

极简代码示例

python 复制代码
class MyList:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, i):      # 被 对象[下标] 时自动执行
        return self.data[i]

m = MyList([10, 20, 30])
print(m[0])

运行结果

text 复制代码
10

附带的三个能力

实测确认,只写这一个方法,就白送了三种能力:

text 复制代码
m[2]        -> 30          支持正下标
m[-1]       -> 30          支持负下标(倒数第一个)
m[0:2]      -> [10, 20]    支持切片
for 遍历:    10 20 30      支持 for 循环
20 in m     -> True        支持 in 判断

其中最反直觉的一点 :只要写了 __getitem__即使不写 __iter__,对象也能被 for 遍历。实测:

python 复制代码
class Counter:
    def __init__(self, n):
        self.n = n

    def __getitem__(self, i):      # 只有这个方法,也能被 for 遍历
        if i >= self.n:
            raise IndexError       # 用 IndexError 告诉 for"结束了"
        return i * i

c = Counter(4)
print([x for x in c])              # [0, 1, 4, 9]

for 会从下标 0 开始不断要值,直到你抛 IndexError 为止,它理解为"没有更多了"。

初学者易错点

易错点 1:忘了抛 IndexError

python 复制代码
def __getitem__(self, i):
    return self.data[i]        # 如果 data 是列表,列表自己会抛,没问题

但如果你自己手写逻辑而不抛异常:

python 复制代码
def __getitem__(self, i):
    return i * i               # 永远不抛异常 → for 循环不会停!

这会导致 for 无限循环,程序卡死。

易错点 2:想用 [] 修改值,却只写了 __getitem__

python 复制代码
m[0] = 99          # 报错:不支持赋值

赋值需要另外写 __setitem__,删除需要 __delitem__。初学阶段先掌握取值即可。

易错点 3:raise IndexError 写成 return IndexError

return 只是把异常类当普通值返回了,for 依然不会停。必须用 raise


3.6 __call__:让对象能像函数一样被调用

作用

让对象后面直接加括号就能调用,像函数一样。

何时被自动触发

对象(参数) 这种写法。

极简代码示例

python 复制代码
class Adder:
    def __init__(self, n):
        self.n = n

    def __call__(self, x):         # 被 对象() 时自动执行
        return self.n + x

add5 = Adder(5)
print(add5(3))

运行结果

text 复制代码
8

注意 add5(3) 这个写法 ------add5 是一个对象,不是函数,但它后面加了括号还能传参,这就是 __call__ 的作用。

实测确认对象与函数的可调用性一致:

text 复制代码
callable(add5)          -> True     可以像函数一样调用
callable(object())      -> False    普通对象不能加括号调用

初学者易错点

易错点 1:以为 __call__ 就是"创建对象"。

不是。Adder(5) 创建对象时调用的其实是 __init__add5(3) 加上括号时调用的才是 __call__两者是不同时机。

python 复制代码
add5 = Adder(5)     # 这里调 __init__
add5(3)             # 这里调 __call__

易错点 2:不知道它有什么用。

最典型的用途是"记住一个配置,反复使用 "。比如 Adder(5) 先记住 n=5,之后 add5(1)add5(100) 都能用这个 5,不必每次重传。这类对象常被称为"带记忆的函数"。

易错点 3:以为普通对象都能加括号。

不能。没写 __call__ 的对象加括号会报 TypeError: 'X' object is not callable


3.7 __add__:让对象能用 +

作用

让两个对象能用 + 相加,自己决定"相加"的含义。

何时被自动触发

对象a + 对象b 时。

极简代码示例

python 复制代码
class Money:
    def __init__(self, yuan):
        self.yuan = yuan

    def __add__(self, other):      # 被 a + b 时自动执行
        return Money(self.yuan + other.yuan)

    def __str__(self):
        return f"{self.yuan} 元"

a = Money(10)
b = Money(20)
print(a + b)

运行结果

text 复制代码
30 元

关键在 __add__ 里返回了什么 ------它返回的是一个新的 Money 对象 ,而不是数字 30。这样连续相加也没问题。

初学者易错点

易错点 1:不写 __add__ 就想用 +

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

Plain(1) + Plain(2)

实测报错:

text 复制代码
TypeError: unsupported operand type(s) for +: 'Plain' and 'Plain'

这句话的意思是"我不知道怎么把两个 Plain 加起来"。Python 不会猜你的意图 ,必须自己写 __add__ 告诉它。

易错点 2:忘了 other 参数。

python 复制代码
def __add__(self):          # 少了 other,a + b 会报错
    return self.yuan

+ 是两个对象参与,所以 __add__(self, other) 必须收两个:self 是左边的,other 是右边的。

易错点 3:self.yuan + other 写错成 self.yuan + other.yuan 之外的形式。

只写 other 的时候,other 是整个对象,不是数字。要取里面的值得写 other.yuan

易错点 4:以为所有运算符都能直接用。

+-*/<> 等等每个运算符都对应一个独立的魔术方法(__add____sub____mul__......),写哪个才能用哪个。


3.8 __eq__:让对象能用 == 比较

作用

决定两个对象"相等"是什么意思。

何时被自动触发

对象a == 对象b 时。

极简代码示例

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

    def __eq__(self, other):       # 被 == 时自动执行
        return self.x == other.x and self.y == other.y

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)

运行结果

text 复制代码
True

对比:不写 __eq__ 会怎样

这是理解 __eq__ 最好的对照。实测:

text 复制代码
不写 __eq__:
  两个内容相同的对象比较  -> False    ← 即使内容一样也是 False!
  对象和自己比较          -> True

这是初学者最容易困惑的地方 :默认情况下,== 比较的不是"内容是否相同",而是"是不是同一个对象"。

所以:

python 复制代码
Raw(1) == Raw(1)        # False,这是两个不同的对象
r = Raw(1)
r == r                  # True,这是同一个对象

只有写了 __eq__== 才会去比较内容。

初学者易错点

易错点 1:以为 == 默认比较内容。

如上所述,默认比较的是"是不是同一个"。想让内容相同的对象判为相等,必须自己写 __eq__

易错点 2:忘记判断类型,拿对象和数字比。

python 复制代码
p1 = Point(1, 2)
p1 == 5

实测报错:

text 复制代码
AttributeError: 'int' object has no attribute 'x'

因为 5 没有 .x 属性。安全写法是先判断类型

python 复制代码
def __eq__(self, other):
    if not isinstance(other, Point):     # 不是 Point 就不比
        return NotImplemented
    return self.x == other.x and self.y == other.y

NotImplemented 是 Python 的特殊约定写法,初学阶段知道要加这个判断即可。)

易错点 3:只写了 __eq__,结果对象不能放进集合了。

写了 __eq__ 之后,Python 会认为这个对象"默认不可哈希",于是不能放进 set 或作字典的键。初学阶段如果遇到"我的对象怎么放进 set 就报错",回头看看是不是写了 __eq__ 但没写 __hash__


3.9 __del__:对象被回收时的收尾

作用

对象被销毁(回收)时自动执行的收尾动作。

何时被自动触发

  • del 删掉最后一个引用时;
  • 程序结束时,所有还活着的对象被统一回收时。

极简代码示例

python 复制代码
class Temp:
    def __init__(self, name):
        self.name = name
        print(f"    创建 {self.name}")

    def __del__(self):             # 对象被回收时自动执行
        print(f"    回收 {self.name}")

t = Temp("对象A")
del t                              # 删掉引用,对象被回收

运行结果

text 复制代码
    创建 对象A
    回收 对象A

初学者易错点

易错点 1:以为 del t 是"删除对象"。

更准确的理解是:del t 删掉的是"名字 t 对对象的引用" 。只有当没有任何名字再指向这个对象时,对象才会被回收,__del__ 才执行。

python 复制代码
a = Temp("X")
b = a                  # 现在两个名字指向同一个对象
del a                  # 只是少了一个名字,对象还在,__del__ 不执行
del b                  # 最后一个名字也没了,这时才回收

易错点 2:以为 __del__ 会立刻、准时执行。

这是最重要的一条。实测验证:

python 复制代码
def make():
    x = Temp("函数内对象")
    return "函数返回"

print(make())

实测输出:

text 复制代码
    创建 函数内对象
    回收 函数内对象
调用函数 -> 函数返回

注意顺序:"回收"那行虽然写在函数里,却是等到函数返回之后才打印的。 而且它出现的时机由 Python 的垃圾回收决定,你无法精确控制。

易错点 3:用 __del__ 做重要的清理工作(例如关文件、存数据)。

不要这么做。 因为触发时机不可控,程序可能在回收前就退出了,导致清理没执行。

需要"保证一定执行的清理"时,用 with 语句(对应 __enter__ / __exit__ 两个魔术方法),这是初学阶段值得养成的习惯,本文不展开。

易错点 4:把 __del____delete__ 搞混。

是两个不同的东西。__del__ 管对象回收,__delete__ 管属性删除。初学阶段只需记住 __del__


3.10 综合示例:把学到的串起来

下面这个类同时用了前面 6 个魔术方法,可以对照理解它们如何协作:

python 复制代码
class Student:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    def __str__(self):
        return f"{self.name},共 {len(self.scores)} 门课"

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

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

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

s = Student("小明", [90, 85, 77])
print(s)
print("门数:", len(s))
print("第一门:", s[0])
print("成绩遍历:", [x for x in s])
print("同名相等:", s == Student("小明", [1, 2]))

运行结果

text 复制代码
小明,共 3 门课
门数: 3
第一门: 90
成绩遍历: [90, 85, 77]
同名相等: True

逐行对应

代码 触发的魔术方法
print(s) __str__
len(s) __len__
s[0] __getitem__
[x for x in s] __getitem__(借它实现了遍历)
s == Student(...) __eq__

同一个对象,配合不同的运算符,自动调用了不同的魔术方法------这就是"魔术"两个字的由来。


第四部分:速查表

常用魔术方法

方法名 触发场景 示例表达式
__init__ 创建对象 d = Dog("旺财")
__str__ 转成给用户看的文字 print(d)
__repr__ 转成给程序员看的文字 repr(d)[d]
__len__ 求长度 len(d)
__getitem__ 用下标取值 d[0]
__setitem__ 用下标赋值 d[0] = 9
__delitem__ 用下标删除 del d[0]
__contains__ in 判断 x in d
__iter__ for 遍历 for x in d:
__call__ 像函数一样调用 d(3)
__add__ 加法 d1 + d2
__sub__ 减法 d1 - d2
__mul__ 乘法 d * 3
__eq__ == 比较 d1 == d2
__lt__ < 比较 d1 < d2
__bool__ 真假判断 if d:
__del__ 对象被回收 del d

内建类型里同样的魔术方法

表达式 实际调用
len([1, 2]) list.__len__
[1, 2][0] list.__getitem__
"a" + "b" str.__add__
3 == 3.0 int.__eq__
str(123) int.__str__
[1] + [2] list.__add__

以上均为实测输出。你早就在使用魔术方法了。

命名规则速查

形式 含义
name 普通名字
_name 约定为内部使用(无强制力)
__name 类内私有,会被改名
__name__ 系统保留,只使用 Python 已定义的

第五部分:给初学者的五条建议

1. 不要试图背完全部

Python 有上百个魔术方法,但日常真正用到的就十几个。本文覆盖的 9 个已经能解决绝大多数需求。剩余的用到再查。

2. 写错了不会报错,是最难查的一类问题

__init__ 写成 _init___str__ 里写成 print 而不是 return------这类错误往往不报异常,只是"没生效" 。养成习惯:写完魔术方法,先用 print 验证它有没有被触发(方法见 2.5 节)。

3. 分清"给用户看"和"给程序员看"

这是初学者最容易混淆的一对。记住:

  • __str__ → 用户,写成 《小王子》
  • __repr__ → 程序员,写成 Book('小王子')
  • 只写一个的话,优先写 __repr__

4. __del__ 不要用来做必要的清理

它的触发时机由垃圾回收决定,你控制不了 。需要"保证执行"的收尾动作,用 with 语句。

5. 每次只加一个魔术方法

不要一次给类加五个魔术方法,然后发现某个不生效却不知道是哪个的问题。加一个、跑一次、确认生效、再加下一个。 这个节奏在初学阶段最稳。


本文档面向零 OOP 基础的 Python 初学者编写

相关推荐
我要见SA姐11 小时前
DeepSeek Harness 开源贡献手记:从 Issue 到 Merge 的完整旅程
ide·vscode·python·自动化·编辑器
沉下心来学鲁班2 小时前
初识DeepAgents搭建第一个智能体
人工智能·python·langchain
ctlover2 小时前
数据结构:树
数据结构·python
触底反弹3 小时前
🐍 Python 语法全景图:一个 print() 引发的深度探索
python
元Y亨H3 小时前
告别依赖地狱与打包崩溃:Python 项目环境治理与 PyInstaller 避坑实践
python
aramae4 小时前
Python 使用库:标准库与第三方库实战
服务器·开发语言·windows·python
IamZJT_4 小时前
Agent 系统工程 01|模型之外,Harness 到底该负责什么?
人工智能·python·程序员
沧海一笑-dj4 小时前
【Python】Python学习笔记-Python 核心基础
人工智能·python·ai·解释型语言
用户76855341255384 小时前
Python 并发怎么选?一篇讲清 threading、multiprocessing、asyncio 的边界
python