🐸 青蛙编程网
首页 / Python / 面向对象 / 4. 魔术方法

魔术方法

这是什么:魔术方法 = 前后各两条下划线的方法,比如 __init__,你不用手写调用,Python 到点自动执行。

为什么用:前面见过的 __init__ 只能算开场。想要 print(狗) 打出名字、让列表能数出长度,光靠普通方法办不到,得给对象开通隐藏能力。

如何用:本章挑 __str__、__repr__、__len__、__eq__ 这几个最常用的,先看它们各自在什么时机被触发。

五个最常用的

魔术方法 触发时机 用途
__init__ 实例化瞬间 初始化属性
__str__ print(对象) 给用户看的描述
__repr__ 调试器、打印列表时 给程序员看的描述
__len__ len(对象) 让对象支持"数个数"
__eq__ 对象 == 对象 自定义相等规则

不定义 __str__,print(dog) 只会输出 <__main__.Dog object at 0x...> 这种机器话。来看五个魔术方法一起干活:

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __str__(self):                    # print(书) 时显示
        return f"《{self.title}》售价{self.price}元"

    def __repr__(self):                   # 调试视角
        return f"Book({self.title!r}, {self.price})"

    def __eq__(self, other):              # 自定义 == 的规则
        return self.title == other.title


b1 = Book("小王子", 25)
b2 = Book("小王子", 30)

print(b1)               # 触发 __str__
print(b1 == b2)         # 触发 __eq__:书名相同就算"相等"

shelf = [b1, Book("活着", 45)]
print(shelf)            # 列表里的对象显示 __repr__
print(len(shelf))
《小王子》售价25元
True
[Book('小王子', 25), Book('活着', 45)]
2
现象 背后的魔术
print(b1) 显示书名和价格 print 自动调 __str__,显示它的返回值
b1 == b2 结果是 True == 自动调 __eq__,按我们写的规则只比书名
列表打印出 Book('小王子', 25) 打印容器时,里面的元素走的是 __repr__

再给自定义类开通 len() 能力:

class Playlist:
    def __init__(self, songs):
        self.songs = songs

    def __len__(self):              # 让 len() 认识它
        return len(self.songs)


my_songs = Playlist(["晴天", "稻香", "夜曲"])
print(len(my_songs))
3

记忆口诀:双下划线不用背,特定时机自动调;str 给用户,repr 给程序员,len 管个数,eq 管相等。

❓ 常见问题

Q1:print(book) 输出 <__main__.Book object at 0x0000021A...>,怎么让它说人话?

原因:类里没定义 __str__,Python 只能报"门牌号"——类名加内存地址,这是出厂默认设置,不是坏了。 修复三步:① 在类里加 def __str__(self):;② 用 return 返回一句 f-string 人话(注意是 return,不是 print);③ 之后 print(book) 就显示这段文字。 举一反三:自定义类都建议配 __str__,print 出来能读,排查问题时省一半力气。

Q2:报 TypeError: __str__ returned non-string (type NoneType)?

原因:__str__ 里用了 print(...) 而不是 return ...。print 只是把文字"念"到屏幕上,自己两手空空——Python 要的是返回值,收到的却是 None,当场翻脸。 修复三步:① 打开 __str__ 方法;② 把 print 换成 return;③ 确认返回的是字符串(f-string 正好是)。 举一反三:魔术方法大多靠返回值交作业,写完检查有没有 return,是这类方法的通用自查项。

Q3:__len__ 返回负数或小数,会怎样?

原因:会报错。len 的合同是"返回一个 ≥ 0 的整数":返回 -1 报 ValueError: __len__() should return >= 0;返回 2.5 报 TypeError: 'float' object cannot be interpreted as an integer。 修复三步:① 检查 __len__ 的 return 表达式;② 确保结果是非负整数,必要时用 len(...) 包一层;③ 想"表达没有"就返回 0。 举一反三:定义魔术方法等于和 Python 签合同,返回类型不按合同来,运行时当场违约。

Q4:b1 == 25 报 AttributeError: 'int' object has no attribute 'title'?

原因:__eq__ 里写了 self.title == other.title,而这回 == 的对面是数字 25——数字身上没有 title 属性,取的那一下就炸了。自定义 __eq__ 不能假设对面一定是同类对象。 修复三步:① 想清楚 == 两边可能出现什么类型;② 比较前先验明正身:if not isinstance(other, Book): return False;③ 再比较属性。 举一反三:写自定义比较规则都要做防呆——isinstance 检查是标配,防的就是"拿苹果和橘子比"。

Q5:__str__ 和 __repr__ 长得像,只写一个行不行?

原因:行,程序不报错,但两个场合看到的会不一样:print 走 __str__(没定义就退回 __repr__,再没定义就显示地址);而打印列表时,里面的元素永远走 __repr__——只写一个,另一个场合就会露馅。 修复三步:① 分清分工:__str__ 给用户看,__repr__ 给程序员调试看;② 正式代码两个都写;③ 偷懒可以写一行 __repr__ = __str__,让两者说同样的话。 举一反三:调试时打印列表、字典,里面的对象显示成一串地址,十有八九是 __repr__ 没定义。

⚠️ 新手翻车现场

翻车一:str 用 print 交卷

class Book:
    def __init__(self, title):
        self.title = title

    def __str__(self):
        print(f"《{self.title}》")      # 用了 print,没有 return


b = Book("小王子")
print(b)
TypeError: __str__ returned non-string (type NoneType)

逐行分析:错在第 6 行——__str__ 的任务是返回一段文字供 print 使用,这里却用 print 自己念了出来:屏幕会先冒出一行《小王子》,紧接着就炸——因为 print 没有"交回"任何文字(None)。把第 6 行改成 return f"《{self.title}》" 即可。

翻车二:len 忘了 self

class Book:
    def __init__(self, title):
        self.title = title

    def __len__():                     # 签名忘了 self
        return len(self.title)


b = Book("小王子")
print(len(b))
TypeError: Book.__len__() takes 0 positional arguments but 1 was given

逐行分析:病根在第 5 行——签名少写了 self;报错在第 10 行引爆——len(b) 触发时 Python 会把对象本身递进来,参数表却没留位置,"多了一件行李"被拒收。魔术方法也是方法,self 一个都不能少。

翻车三:f-string 忘了 f

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price

    def __repr__(self):
        return "Book({self.title!r}, {self.price})"     # 引号前少了 f


shelf = [Book("小王子", 25)]
print(shelf)
[Book({self.title!r}, {self.price})]

逐行分析:错在第 7 行——引号前少了 f,花括号没开启"填坑模式",整段被当成普通文字原样输出。它不报错,但结果驴唇不对马嘴——这种无声翻车更难查,输出一不对劲就回头检查 f。

📦 完整小示例

给电影类开通"魔术全家桶":print、==、len 各有各的机关。

class Movie:
    def __init__(self, title, score):
        self.title = title
        self.score = score

    def __str__(self):
        return f"《{self.title}》评分 {self.score}"

    def __repr__(self):
        return f"Movie({self.title!r}, {self.score})"

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

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


m1 = Movie("流浪地球", 8.0)
m2 = Movie("流浪地球", 7.5)

print(m1)              # 触发 __str__
print(m1 == m2)        # 触发 __eq__:同名即同片
print(len(m1))         # 触发 __len__:片名字数
watched = [m1, Movie("哪吒之魔童闹海", 8.4)]
print(watched)         # 列表元素显示 __repr__

你会看到:

《流浪地球》评分 8.0
True
4
[Movie('流浪地球', 8.0), Movie('哪吒之魔童闹海', 8.4)]

逐段中文精讲

  • 第 1~4 行(出生登记):__init__ 本身就是最早认识的魔术方法——Movie(...) 一执行它自动开跑,把片名、评分挂到对象身上。
  • 第 6~10 行(str 与 repr 分工):__str__ 的输出给人看;__repr__ 带类名、带引号,给调试看。两者都用 return 交卷。
  • 第 11~12 行(自定义相等):__eq__ 只比片名——同一部片不同评分也算"相等",适合"看没看过"这类去重判断。
  • 第 14~15 行(数片名):__len__ 转手把片名交给 len 数数,"流浪地球"4 个字,返回 4。
  • 第 17~21 行(逐一触发):print(m1) 走 str;== 走 eq 得 True;len 得 4;打印列表时元素换上"repr 制服"——四个魔术方法在同一屏各显神通。

跑通后做个小实验:把 __eq__ 的比较改成 self.title == other.title and self.score == other.score,再看 m1 == m2 变成 False——相等的规则是你定的,改一行,全程序的 == 都跟着变。

🎯 什么时候用它

  • 商品打印:电商后台打印购物车列表,靠 __repr__ 显示"商品名 + 价格"而不是一串内存地址,对账一眼清。
  • 查重去重:__eq__ 定好"歌名 + 歌手相同就算同一首",合并歌单时一句 == 就能查重。
  • 容器类开挂:自定义"歌单""书架"配上 __len__,len(playlist) 直接报数量,和原生列表一样顺手。

📝 本章小结

  • 魔术方法从不手动调用:print 触发 __str__、== 触发 __eq__、len 触发 __len__、实例化触发 __init__
  • __str__ 给用户看,__repr__ 给程序员调试看;打印列表时元素走 __repr__
  • 魔术方法靠 return 交作业;__len__ 必须返回非负整数
  • 记忆锚点:魔术方法是自动门——你只管走过去(print、len、==),门自己开。

动手改

  1. 给 Book 加 __len__,返回书名字符数,print(len(b1)) 验证(提示:len(self.title))
  2. 把 __repr__ 改成返回和 __str__ 一样的文字,再打印 shelf 对比效果
  3. 新建 Cart 类:__init__ 收一个商品列表,__len__ 返回商品数,装 3 件商品后 print(len(cart))

下一章:综合练习,把五章所学串成一个宠物管理系统 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。