魔术方法
这是什么:魔术方法 = 前后各两条下划线的方法,比如 __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、==),门自己开。
动手改
- 给
Book加__len__,返回书名字符数,print(len(b1))验证(提示:len(self.title)) - 把
__repr__改成返回和__str__一样的文字,再打印shelf对比效果 - 新建
Cart类:__init__收一个商品列表,__len__返回商品数,装 3 件商品后print(len(cart))
下一章:综合练习,把五章所学串成一个宠物管理系统 🐸