第6章 6.3 魔术方法与魔术常量

🎯 开场:为什么你的代码像在"自动驾驶"?

上一章我们学了继承和接口,写出了有模有样的类结构。但你有没有这种感觉——有些代码写起来特别"自动",比如:

print(对象)  # 自动调用某个方法,输出好看的格式
len(对象)    # 自动知道这个对象有多长
对象.不存在的属性  # 居然不报错,而是返回了一个默认值

这种"明明没写这个方法,但它自动就工作了"的感觉,就是魔术方法在背后偷偷帮你做了事。

痛点来了:你是不是也见过这种代码——自己写的类,print 出来是一串乱码 <__main__.User object at 0x7f...>,根本看不懂;或者想把两个对象用 + 加起来,结果报错;再或者访问一个不存在的属性,直接崩溃了。

学完这一章,你就能:

  • print(对象) 输出你想让人看的内容
  • 让对象支持 len()+[] 这种运算符操作
  • 优雅地处理"属性不存在"的情况,不让程序崩溃
  • 理解 Pyth\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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 讲

互动钩子:你在实际项目里用过哪个魔术方法?遇到过什么坑?评论区聊聊,老粉优先回复!


📌 下章预告:学会了魔术方法,你的对象已经"能说会道"了。但如果你想让类的创建方式更灵活(一个类能产出不同形态的对象),或者让多个对象之间"心灵感应"(一个对象变了,其他对象自动知道),下一章「设计模式:单例/工厂/观察者」会给你答案。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。