第6章 6.3 魔术方法与魔术常量
🎯 开场:为什么你的代码像在"自动驾驶"?
上一章我们学了继承和接口,写出了有模有样的类结构。但你有没有这种感觉——有些代码写起来特别"自动",比如:
print(对象) # 自动调用某个方法,输出好看的格式
len(对象) # 自动知道这个对象有多长
对象.不存在的属性 # 居然不报错,而是返回了一个默认值
这种"明明没写这个方法,但它自动就工作了"的感觉,就是魔术方法在背后偷偷帮你做了事。
痛点来了:你是不是也见过这种代码——自己写的类,print 出来是一串乱码 <__main__.User object at 0x7f...>,根本看不懂;或者想把两个对象用 + 加起来,结果报错;再或者访问一个不存在的属性,直接崩溃了。
学完这一章,你就能:
- 让
print(对象)输出你想让人看的内容 - 让对象支持
len()、+、[]这种运算符操作 - 优雅地处理"属性不存在"的情况,不让程序崩溃
- 理解 Pyth\n\n
\n\n
\n\non 框架里那些"约定俗成"的写法(比如 Django 的 ORM 为啥能 User.objects.get())
🧱 基础:4 个必须懂的魔术方法
概念速览
魔术方法就是名字以双下划线开头和结尾的特殊函数。Python 解释器在特定时刻会自动调用它们。
生活类比:就像机场的"快捷登机口",你不用主动去调用它,当你拿着特定机票(触发特定操作)走过时,门就自动开了。
1. __init__ — 对象的"构造函数"
是什么:创建对象时自动调用的初始化方法。
为什么要用:每个对象创建时都需要"设置默认值"、"接收参数",这是对象诞生的"脐带"。
怎么用:
class 小明:
def __init__(self, 姓名, 年龄):
self.姓名 = 姓名
self.年龄 = 年龄
xiaoming = 小明("小明", 8)
print(f"姓名:{xiaoming.姓名},年龄:{xiaoming.年龄}岁")
输出:
姓名:小明,年龄:8岁
解释:创建
xiaoming时,自动调用__init__,把姓名和年龄绑定到这个对象上。
2. __str__ — 对象的"名片"
是什么:当你用 print(对象) 时,Python 会调用这个方法获取"人类可读"的字符串。
为什么要用:没有 __str__,print 对象会得到一串内存地址,看不懂。
怎么用:
class 商品:
def __init__(self, 名称, 价格):
self.名称 = 名称
self.价格 = 价格
def __str__(self):
return f"{self.名称}(¥{self.价格})"
苹果 = 商品("红富士苹果", 5.5)
print(苹果) # 不再是乱码,而是一张"名片"
输出:
红富士苹果(¥5.5)
解释:print 时自动调用
__str__,返回一个人能看懂的字符串。
3. __len__ — 让对象支持 len()
是什么:让自定义对象也能用 len() 获取长度。
为什么要用:列表有长度、字符串有长度,你写的"购物车"对象也该有长度——代表里面有多少商品。
怎么用:
class 购物车:
def __init__(self):
self.商品列表 = []
def 添加商品(self, 商品名):
self.商品列表.append(商品名)
def __len__(self):
return len(self.商品列表)
cart = 购物车()
cart.添加商品("牛奶")
cart.添加商品("面包")
print(f"购物车里有 {len(cart)} 件商品")
输出:
购物车里有 2 件商品
解释:
len(cart)自动调用__len__,返回商品数量。
4. __getitem__ — 让对象支持 [] 访问
是什么:让对象可以用 对象[下标] 的方式访问。
为什么要用:列表能用 list[0] 访问,你的"成绩单"对象也该能用 成绩单["语文"] 访问各科成绩。
怎么用:
class 成绩单:
def __init__(self):
self.成绩 = {"语文": 92, "数学": 88, "英语": 95}
def __getitem__(self, 科目):
return self.成绩.get(科目, "无成绩")
report = 成绩单()
print(f"语文成绩:{report['语文']}")
print(f"体育成绩:{report['体育']}") # 不存在的科目,返回"无成绩"
输出:
语文成绩:92
体育成绩:无成绩
解释:
report['语文']自动调用__getitem__,取出对应科目的成绩。
5. __call__ — 把对象当函数调用
是什么:让对象可以像函数一样用 对象() 来调用。
为什么要用:有时候"一个行为"太简单,不值得单独写个函数,但又想有个"可调用的动作"。比如计时器、过滤器。
怎么用:
class 计时器:
def __call__(self, 动作):
print(f"⏱️ {动作},用时 2.5 秒")
timer = 计时器()
timer("下载文件") # 像调用函数一样调用对象
输出:
⏱️ 下载文件,用时 2.5 秒
解释:
timer()触发了__call__,把对象变成了"可调用对象"。
6. __repr__ — 调试用的"技术名片"
是什么:__str__ 是给人看的,__repr__ 是给开发者看的。主要在调试、log 里出现。
为什么要用:有时候你需要两种输出——一个是普通用户看的(__str__),一个是开发者 debug 用的(__repr__)。
怎么用:
class 学生:
def __init__(self, 姓名, 学号):
self.姓名 = 姓名
self.学号 = 学号
def __repr__(self):
return f"学生(姓名='{self.姓名}', 学号={self.学号})"
s = 学生("小红", 1001)
print(repr(s)) # 开发者视角:完整的构造函数式输出
输出:
学生(姓名='小红', 学号=1001)
解释:
repr(s)调用__repr__,输出"可以直接复制运行"的字符串。
🔥 实战:3 个递进项目
项目 1:做一个"可打印的成绩单"(5 分钟)
需求:创建一个学生成绩类,支持直接 print 查看成绩,不懂技术的人也能看懂。
完整代码:
class 成绩单:
def __init__(self, 姓名, 成绩字典):
self.姓名 = 姓名
self.成绩 = 成绩字典
def __str__(self):
lines = [f"📚 {self.姓名} 的成绩单", "-" * 15]
for 科目, 分数 in self.成绩.items():
lines.append(f" {科目}:{分数}分")
lines.append("-" * 15)
平均分 = sum(self.成绩.values()) / len(self.成绩)
lines.append(f" 平均分:{平均分:.1f}分")
return "\n".join(lines)
# 使用
成绩 = 成绩单("张小明", {"语文": 88, "数学": 92, "英语": 85})
print(成绩)
预期输出:
📚 张小明的成绩单
---------------
语文:88分
数学:92分
英语:85分
---------------
平均分:88.3分
一句话解释:__str__ 把对象变成了一封格式整齐的"成绩单",直接 print 就能看。
项目 2:做一个"购物车"(15 分钟)
需求:做一个购物车类,支持添加商品、查看数量、查看总价、支持 len() 和 in 操作符。
完整代码:
import json
class 商品:
def __init__(self, 名称, 单价, 数量=1):
self.名称 = 名称
self.单价 = 单价
self.数量 = 数量
@property
def 小计(self):
return self.单价 * self.数量
def __repr__(self):
return f"商品('{self.名称}', 单价={self.单价}, 数量={self.数量})"
class 购物车:
def __init__(self):
self.商品列表 = []
def 添加商品(self, 商品):
self.商品列表.append(商品)
def 删除商品(self, 商品名):
self.商品列表 = [g for g in self.商品列表 if g.名称 != 商品名]
def __len__(self):
return len(self.商品列表)
def __contains__(self, 商品名):
return any(g.名称 == 商品名 for g in self.商品列表)
def __getitem__(self, 索引):
return self.商品列表[索引]
@property
def 总价(self):
return sum(g.小计 for g in self.商品列表)
def __str__(self):
if not self.商品列表:
return "🛒 购物车是空的"
lines = ["🛒 购物车内容:"]
for i, g in enumerate(self.商品列表):
lines.append(f" {i+1}. {g.名称} x {g.数量} = ¥{g.小计}")
lines.append(f" ──────────────")
lines.append(f" 总计:{self.总价} 元")
return "\n".join(lines)
# 演示
cart = 购物车()
cart.添加商品(商品("红富士苹果", 5.5, 3))
cart.添加商品(商品("全麦面包", 12.0, 1))
cart.添加商品(商品("纯牛奶", 3.5, 2))
print(cart)
print(f"\n购物车共 {len(cart)} 种商品")
print(f"'红富士苹果' 在购物车里吗?{'是' if '红富士苹果' in cart else '否'}")
print(f"第二种商品:{cart[1]}")
预期输出:
🛒 购物车内容:
1. 红富士苹果 x 3 = ¥16.5
2. 全麦面包 x 1 = ¥12.0
3. 纯牛奶 x 2 = ¥7.0
──────────────
总计:35.5 元
购物车共 3 种商品
'红富士苹果' 在购物车里吗?是
第二种商品:商品('全麦面包', 单价=12.0, 数量=1)
一句话解释:__len__、__contains__、__getitem__ 让购物车用起来跟列表一样自然。
项目 3:做一个"学生管理系统"(15 分钟)
需求:做一个学生管理类,从 CSV 文件读取学生数据,支持查询、添加、统计功能。
准备数据文件 students.csv:
姓名,语文,数学,英语
张小明,88,92,85
李小红,95,88,91
王小强,76,85,90
完整代码:
import csv
from pathlib import Path
class 学生:
def __init__(self, 姓名, 语文, 数学, 英语):
self.姓名 = 姓名
self.语文 = int(语文)
self.数学 = int(数学)
self.英语 = int(英语)
@property
def 平均分(self):
return (self.语文 + self.数学 + self.英语) / 3
@property
def 总分(self):
return self.语文 + self.数学 + self.英语
def __str__(self):
return f"{self.姓名}(语文{self.语文} 数学{self.数学} 英语{self.英语},平均{self.平均分:.1f})"
def __repr__(self):
return f"学生('{self.姓名}', {self.语文}, {self.数学}, {self.英语})"
def to_dict(self):
return {"姓名": self.姓名, "语文": self.语文, "数学": self.数学, "英语": self.英语, "平均分": round(self.平均分, 1)}
class 学生管理器:
def __init__(self):
self.学生列表 = []
@classmethod
def 从_csv加载(cls, 文件路径):
管理器 = cls()
with open(文件路径, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for 行 in reader:
s = 学生(行["姓名"], 行["语文"], 行["数学"], 行["英语"])
管理器.学生列表.append(s)
return 管理器
def __len__(self):
return len(self.学生列表)
def __getitem__(self, 索引):
return self.学生列表[索引]
def __contains__(self, 姓名):
return any(s.姓名 == 姓名 for s in self.学生列表)
def 添加学生(self, 学生):
if 学生.姓名 in self:
print(f"⚠️ 学生 {学生.姓名} 已存在")
return False
self.学生列表.append(学生)
return True
def 查询学生(self, 姓名):
for s in self.学生列表:
if s.姓名 == 姓名:
return s
return None
def 统计(self):
if not self.学生列表:
return "无学生数据"
总分平均 = sum(s.平均分 for s in self.学生列表) / len(self.学生列表)
最高分学生 = max(self.学生列表, key=lambda s: s.总分)
return f"学生总数:{len(self)}\n班级平均分:{总分平均:.1f}\n总分最高:{最高分学生.姓名}({最高分学生.总分}分)"
def __str__(self):
return "\n".join(str(s) for s in self.学生列表)
# 演示
manager = 学生管理器.从_csv加载("students.csv")
print("=== 所有学生 ===")
print(manager)
print("\n=== 统计信息 ===")
print(manager.统计())
print("\n=== 添加新生 ===")
new_student = 学生("陈新", 90, 92, 88)
manager.添加学生(new_student)
print(manager)
预期输出:
=== 所有学生 ===
张小明(语文88 数学92 英语85,平均88.3)
李小红(语文95 数学88 英语91,平均91.3)
王小强(语文76 数学85 英语90,平均83.7)
=== 统计信息 ===
学生总数:3
班级平均分:87.8
总分最高:李小红(274分)
=== 添加新生 ===
陈新(语文90 数学92 英语88,平均90.0)
一句话解释:魔术方法让自定义类跟 Python 内置类型用起来一样自然,len()、in、[] 全都支持。
💪 进阶:常见坑 + 调试技巧
坑 1:__str__ 和 __repr__ 傻傻分不清
# ❌ 错误:只写了 __repr__,但 print 用的是 __str__,可能输出不好看的地址
class 错误例子:
def __repr__(self):
return "正确的输出"
# ✅ 正确:如果只写一个,两个都用这个,推荐写 __repr__
class 正确例子:
def __repr__(self):
return "正确的输出"
# __str__ 没写,Python 会自动 fallback 到 __repr__
坑 2:构造函数里漏了 self
# ❌ 错误:忘了加 self
class 错误:
def __init__(姓名, 年龄): # 少了 self
姓名 = 年龄 # 这只是局部变量,不是对象的属性
# ✅ 正确:self 是指向实例自己的引用
class 正确:
def __init__(self, 姓名, 年龄):
self.姓名 = 姓名
self.年龄 = 年龄
坑 3:__len__ 返回非整数
# ❌ 错误:len() 必须返回整数
class 错误:
def __len__(self):
return 3.14 # 报错!
# ✅ 正确:返回整数
class 正确:
def __len__(self):
return 3
坑 4:在 __getattr__ 里无限递归
# ❌ 错误:访问不存在的属性时又去访问它,死循环
class 错误:
def __getattr__(self, name):
return self.name # 访问 self.name 又触发 __getattr__
# ✅ 正确:抛出 AttributeError 表示属性不存在
class 正确:
def __getattr__(self, name):
return f"属性 {name} 不存在"
坑 5:忘记 __init__ 不是唯一的构造函数
# ❌ 错误:以为创建对象一定会调用自己的 __init__
class 错误:
def __init__(self, x):
self.x = x
# 如果子类重写了 __init__,父类的不会被自动调用
# ✅ 正确:用 super() 确保父类初始化被调用
class 正确:
def __init__(self, x):
self.x = x
super().__init__()
调试技巧:用一个"调试模式"属性
class 学生:
def __init__(self, 姓名):
self.姓名 = 姓名
self._debug = False # 调试开关
def __repr__(self):
if self._debug:
return f"<学生 object '{self.姓名}' at {hex(id(self))}>"
return f"学生('{self.姓名}')"
s = 学生("小明")
print(repr(s)) # 正常输出:学生('小明')
s._debug = True
print(repr(s)) # 调试输出:<学生 object '小明' at 0x...>
✏️ 练习题
练习 1(1 分钟):抄改构造函数
- 输入:创建一个 Book 类,初始化时传入书名和作者
- 预期输出:print 对象时输出类似 "《活着》- 余华"
- 提示:修改 __str__ 方法的返回值格式
练习 2(2 分钟):添加判断逻辑
- 输入:在练习 1 基础上,如果作者是"未知",输出 "《书名》- 作者待定"
- 预期输出:Book("无题", "未知") → "《无题》- 作者待定"
- 提示:在 __str__ 里加个 if 判断
练习 3(3 分钟):支持 len()
- 输入:给购物车加上 __len__ 方法,返回商品总数(不是种类数)
- 预期输出:len(cart) 返回购物车里所有商品的数量之和
- 提示:累加每个商品的 数量 属性
练习 4(4 分钟):组合练习 1-3
- 输入:创建一个 CartItem 类(商品+数量),支持 __str__、__len__、__getitem__
- 预期输出:能够 print 出来,也能按索引访问,也能 len()
- 提示:参考项目 2 的代码结构
练习 5(5 分钟):分析报错
- 输入:下面代码为什么会报错?
class A:
def __len__(self):
return "长度"
a = A()
print(len(a))
- 预期输出:解释错误原因并给出修复方案
- 提示:
len()函数要求返回值是整数
作业:做一个「通讯录管理系统」
需求描述:做一个命令行通讯录,支持增删改查,所有操作持久化到 JSON 文件。
功能点:
1. 添加联系人(姓名、手机号、邮箱)
2. 删除联系人
3. 按姓名查询联系人
4. 列出所有联系人
5. 支持 len(通讯录) 查看联系人数
加分项:
1. 支持用 通讯录[姓名] 直接获取联系人
2. 导出联系人到 CSV 文件
验收标准:
- 能跑起来,运行不报错
- 添加后重启程序数据还在(持久化)
- len() 返回正确的联系人数
提交方式:评论区贴代码或 GitHub 链接
📚 总结
这一章我们学了 6 个最常用的魔术方法(__init__、__str__、__repr__、__len__、__getitem__、__call__),它们让自定义对象用起来跟 Python 内置类型一样自然。
延伸学习资源:
- 官方文档:Python 数据模型(最权威的魔术方法手册)
- 《Python 编程:从入门到实践》第 9 章(面向对象编程)
- 视频:B 站「小甲鱼」Python 教程第 39-40 讲
互动钩子:你在实际项目里用过哪个魔术方法?遇到过什么坑?评论区聊聊,老粉优先回复!
📌 下章预告:学会了魔术方法,你的对象已经"能说会道"了。但如果你想让类的创建方式更灵活(一个类能产出不同形态的对象),或者让多个对象之间"心灵感应"(一个对象变了,其他对象自动知道),下一章「设计模式:单例/工厂/观察者」会给你答案。

评论(0)