第7章 7.4 类方法/静态方法/魔术方法

🎯 开场:为什么要学这个?

上一章我们学会了用「继承」让代码复用,用「多态」让不同对象说同一种话。但你有没有遇到过这种情况:

  • 想给一个类直接调用,不需要先 new 一个对象?
  • 想让两个对象比较大小,但不知道怎么让 == 生效?
  • 想让自己的类打印出来好看一点,而不是显示 <__main__.Student object at 0x7fxxx>

这些场景,类方法、静态方法、魔术方法 就是你的解决方案。

学完这章,你能:
1. 用 @classmethod 写工厂方法,一行代码创建对象
2. 用 @staticmethod 写工具函数,不依赖实例也能调用
3. 用 __str__/__repr__/__eq__ 让你的对象更好看、更好用


🧱 基础:三个概念一次讲透

1. 类方法 @classmethod —— 类的专属构造函数

是什么?
类方法是绑定到「类」而不是「实例」的方法。用 @classmethod 装饰器标记。

生活类比:
想象你要寄快递。你可以自己打包(手动创建对象),也可以让快递公司帮你打包(类方法帮你创建)。工厂方法就是「快递公司」——专业的事交给专业的人做。

为什么用?
当你需要用不同方式创建同一个类的对象时,构造函数不够用。类方法可以提供多种「生产线」。

怎么用?

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

# 类方法:另一种创建对象的方式
@classmethod
def from_dict(cls, data):
    """从字典创建学生对象"""
    return cls(data['name'], data['score'])

# 普通方式创建
s1 = Student("小明", 85)

# 类方法方式创建 —— 一行代码搞定
data = {"name": "小红", "score": 92}
s2 = Student.from_dict(data)

print(s2.name, s2.score)  # 输出:小红 92

解释:第 17 行调用 from_dict,类方法自动拿到 Student 类本身(cls),然后用它创建对象。

![配图1 - 配图1](images/inline_230_1.png)


2. 静态方法 @staticmethod —— 不依赖类和实例的工具函数

是什么?
静态方法是「挂靠在类名下」的普通函数。用 @staticmethod 装饰器标记,但它不需要 selfcls 参数。

生活类比:
就像你家楼下的自助快递柜——它不属于任何快递公司,但任何快递都可以用。静态方法就是那个快递柜,跟哪个对象都没关系,但能在类的命名空间里统一管理。

为什么用?
当你有一段逻辑上属于这个类,但不依赖实例属性的代码时用它。把相关功能放在一起,代码更整洁。

怎么用?

class MathTool:
@staticmethod
def calculate_tax(price, rate=0.13):
    """计算税费(跟哪个实例都没关系)"""
    return price * rate

@staticmethod
def format_money(amount):
    """格式化金额为字符串"""
    return f"¥{amount:.2f}"

# 调用静态方法:直接用类名,不需要创建对象
tax = MathTool.calculate_tax(100)
print(tax)              # 输出:13.0
print(MathTool.format_money(99.8))  # 输出:¥99.80

解释:第 13 行直接用 类名.方法名() 调用静态方法,不需要 new MathTool()


3. 魔术方法 —— 让你的对象更聪明

是什么?
魔术方法(也叫「双下方法」或「dunder methods」)是以双下划线 __ 开头和结尾的特殊方法。Python 会在特定场景自动调用它们。

生活类比:
想象你有一个「智能音箱」。你说「现在几点」,它自动播放时间——你不需要手动触发,它在听特定关键词。魔术方法就是那个「关键词触发器」。

3.1 __str__ —— 控制 print 输出的外观

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

def __str__(self):
    """当你 print 这个对象时,Python 会调用这个方法"""
    return f"学生{self.name},成绩{self.score}"

s = Student("小明", 85)
print(s)  # 输出:学生小明,成绩85
print(str(s))  # 效果同上

3.2 __repr__ —— 控制调试时的外观

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

def __repr__(self):
    """给开发者看的,用于调试"""
    return f"Student(name='{self.name}', score={self.score})"

s = Student("小明", 85)
print(repr(s))  # 输出:Student(name='小明', score=85)

注意__str__ 是给用户看的,__repr__ 是给开发者看的。如果只定义 __repr__,print 也会用它。

3.3 __eq__ —— 控制 == 的比较逻辑

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

def __eq__(self, other):
    """定义两个学生什么时候算相等"""
    if not isinstance(other, Student):
        return False
    return self.name == other.name and self.score == other.score

s1 = Student("小明", 85)
s2 = Student("小明", 85)
s3 = Student("小红", 90)

print(s1 == s2)  # 输出:True(内容相同)
print(s1 == s3)  # 输出:False

3.4 __hash__ —— 让对象可以放进 set 或当 dict 的键

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

def __eq__(self, other):
    if not isinstance(other, Student):
        return False
    return self.name == other.name

def __hash__(self):
    """必须和 __eq__ 保持一致:相等的对象必须有相同的哈希值"""
    return hash(self.name)

s1 = Student("小明", 85)
s2 = Student("小明", 90)  # 名字相同,分数不同

# 现在可以放进 set 了
students = {s1, s2}
print(len(students))  # 输出:1(因为 s1 和 s2 被视为同一个)

![配图2 - 配图2](images/inline_230_2.png)


🔥 实战:三个递进项目

项目 1:学生成绩管理器(5 分钟)

需求:创建学生对象,用类方法从不同数据源创建,比较两个学生是否相同。

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

@classmethod
def from_line(cls, line):
    """从 '姓名,分数' 格式的行创建"""
    name, score = line.strip().split(',')
    return cls(name, int(score))

@classmethod
def from_dict(cls, data):
    """从字典创建"""
    return cls(data['name'], data['score'])

def __str__(self):
    return f"📚 {self.name}:{self.score}分"

def __eq__(self, other):
    if not isinstance(other, Student):
        return False
    return self.name == other.name

def __hash__(self):
    return hash(self.name)


# 测试代码
s1 = Student("小明", 85)
s2 = Student.from_line("小红,92")
s3 = Student.from_dict({"name": "小李", "score": 88})

print(s1)
print(s2)
print(s3)

# 比较
s4 = Student("小明", 100)  # 同名,不同分数
print(s1 == s4)  # True(只比较名字)

#放进 set
classroom = {s1, s4}
print(f"班级人数(去重后): {len(classroom)}")

预期输出

📚 小明:85分
📚 小红:92分
📚 小李:88分
True
班级人数(去重后): 1

解释:用类方法从字符串/字典创建对象,用 __str__ 让打印好看,用 __eq____hash__ 实现按名字去重。


项目 2:CSV 批量导入学生数据(15 分钟)

需求:从一个 CSV 文件读取学生数据,批量创建对象,统计平均分,找出最高分学生。

先准备一个 students.csv 文件(注意是真实文件路径):

小明,85
小红,92
小李,88
小张,76
小王,95

完整代码

import csv
from io import StringIO  # 模拟文件,真实环境用 open()

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

@classmethod
def from_csv_line(cls, line):
    """从 CSV 行创建学生"""
    name, score = line.strip().split(',')
    return cls(name, int(score))

@staticmethod
def is_valid_score(score):
    """验证分数是否合法"""
    return 0 <= score <= 100

def __str__(self):
    status = "✅" if self.score >= 60 else "❌"
    return f"{status} {self.name}:{self.score}分"

def __repr__(self):
    return f"Student('{self.name}', {self.score})"


class StudentManager:
def __init__(self):
    self.students = []

def load_from_csv(self, csv_content):
    """从 CSV 内容加载学生数据"""
    reader = csv.reader(StringIO(csv_content))
    for row in reader:
        if len(row) != 2:
            continue
        name, score = row[0], int(row[1])
        if Student.is_valid_score(score):
            self.students.append(Student(name, score))
        else:
            print(f"⚠️ 跳过非法分数:{name}, {score}")

def get_average(self):
    """计算平均分"""
    if not self.students:
        return 0
    total = sum(s.score for s in self.students)
    return total / len(self.students)

def get_top_student(self):
    """获取最高分学生"""
    return max(self.students, key=lambda s: s.score)


# 模拟 CSV 内容(真实环境用 open('students.csv').read())
csv_content = """小明,85
小红,92
小李,88
小张,76
小王,95
小赵,105"""

manager = StudentManager()
manager.load_from_csv(csv_content)

print("=== 学生列表 ===")
for s in manager.students:
print(s)

print(f"\n平均分:{manager.get_average():.1f}")
print(f"最高分:{manager.get_top_student()}")

预期输出

⚠️ 跳过非法分数:小赵, 105
=== 学生列表 ===
✅ 小明:85分
✅ 小红:92分
✅ 小李:88分
✅ 小张:76分
✅ 小王:95分

平均分:87.2
最高分:✅ 小王:95分

解释:静态方法 is_valid_score 验证分数合法性,类方法 from_csv_line 批量创建对象。


项目 3:带数据持久化的待办清单(15 分钟)

需求:一个待办清单,支持添加、完成、查看,能保存到 JSON 文件,下次启动自动加载。

import json
from pathlib import Path

class TodoItem:
def __init__(self, title, completed=False):
    self.title = title
    self.completed = completed

def complete(self):
    self.completed = True

def __str__(self):
    status = "✅" if self.completed else "⬜"
    return f"{status} {self.title}"

def __repr__(self):
    return f"TodoItem('{self.title}', {self.completed})"

def to_dict(self):
    return {"title": self.title, "completed": self.completed}

@classmethod
def from_dict(cls, data):
    return cls(data['title'], data['completed'])


class TodoList:
def __init__(self, filename="todo.json"):
    self.filename = filename
    self.items = []
    self.load()

def add(self, title):
    self.items.append(TodoItem(title))
    self.save()

def complete(self, index):
    if 0 <= index < len(self.items):
        self.items[index].complete()
        self.save()

def show(self):
    if not self.items:
        print("📝 待办清单是空的!")
        return
    print("📝 待办清单:")
    for i, item in enumerate(self.items):
        print(f"  {i+1}. {item}")

def save(self):
    data = [item.to_dict() for item in self.items]
    with open(self.filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False)

def load(self):
    if Path(self.filename).exists():
        with open(self.filename, 'r', encoding='utf-8') as f:
            data = json.load(f)
            self.items = [TodoItem.from_dict(d) for d in data]


# 演示(真实环境运行多次会持久化)
if __name__ == "__main__":
todo = TodoList("my_todo.json")

# 添加几个待办
todo.add("完成 Python 作业")
todo.add("给妈妈打电话")
todo.add("整理房间")

# 完成第二个
todo.complete(1)

# 展示
todo.show()

预期输出(首次运行):

📝 待办清单:
1. ⬜ 完成 Python 作业
2. ✅ 给妈妈打电话
3. ⬜ 整理房间

解释:用 __str__ 控制显示效果,用 to_dict/from_dict 实现 JSON 序列化。文件 my_todo.json 会在当前目录生成,下次运行自动恢复数据。


💪 进阶:常见坑 + 调试技巧

坑 1:__eq____hash__ 不一致

# ❌ 错误示例
class BadStudent:
def __init__(self, name):
    self.name = name


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

# 忘记实现 __hash__,或 hash 和 eq 逻辑不一致
def __hash__(self):
    return hash(self.name + "_suffix")  # 跟 eq 比较的不一样!

# ✅ 正确示例
class GoodStudent:
def __init__(self, name):
    self.name = name

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

def __hash__(self):
    return hash(self.name)  # 必须跟 __eq__ 用相同的字段

原因:Python 规定「相等的对象必须有相等的哈希值」。违反这条会导致 set/dict 出错。


坑 2:静态方法不需要 self 但写了 self

# ❌ 错误示例
class MathTool:
@staticmethod
def bad_add(a, b):
    return self.a + self.b  # 静态方法里没有 self!

# ✅ 正确示例
class MathTool:
@staticmethod
def good_add(a, b):
    return a + b  # 直接用参数

坑 3:__str__ 返回了调试信息

# ❌ 错误示例
class Student:
def __str__(self):
    return f"Student(name='{self.name}')"  # 这是 __repr__ 的风格

# ✅ 正确示例
class Student:
def __str__(self):
    return f"{self.name},分数{self.score}"  # 给用户看的简洁信息

def __repr__(self):
    return f"Student('{self.name}', {self.score})"  # 给开发者看的详细信息

坑 4:类方法第一个参数不是 cls

# ❌ 错误示例
class Student:
@classmethod
def from_dict(cls, data):
    return cls(data['name'], data['score'])

@classmethod
def wrong_method(self, data):  # 类方法第一个参数必须是 cls,不是 self!
    return self.from_dict(data)

# ✅ 正确示例
class Student:
@classmethod
def from_dict(cls, data):
    return cls(data['name'], data['score'])

@classmethod
def from_list(cls, lst):
    return cls(lst[0], lst[1])  # cls 才是正确的第一个参数

调试技巧:用 vars() 查看对象所有属性

class Student:
def __init__(self, name, score):
    self.name = name
    self.score = score

s = Student("小明", 85)
print(vars(s))  # 输出:{'name': '小明', 'score': 85}

# 或者用 __dict__
print(s.__dict__)  # 同上

场景:当你不确定对象里有什么数据时,vars()obj.__dict__ 能快速看到所有实例属性。


✏️ 练习题

练习 1(2 分钟):类方法调用
- 输入:调用 Student.from_dict({"name": "小刚", "score": 78})
- 预期输出:小刚:78分(用 __str__ 格式)
- 提示:直接用项目 1 的 from_dict 类方法

练习 2(3 分钟):添加判断逻辑
- 输入:在项目 1 基础上,添加一个 is_passed() 方法,返回 True/False
- 预期输出:Student("小明", 85).is_passed()TrueStudent("小红", 55).is_passed()False
- 提示:60 分及格

练习 3(5 分钟):处理新数据格式
- 输入:"小明:85,小红:92,小李:88" 用冒号分隔的字符串
- 预期输出:打印三个学生的信息
- 提示:参考 from_line 的写法,用 : 分割而不是 ,

练习 4(8 分钟):组合项目 2 和 3
- 输入:用项目 2 的 CSV 加载功能加载学生,再用项目 3 的 __eq__ 去重
- 预期输出:相同姓名的学生只保留一个
- 提示:利用 __hash____eq__ 实现 set 去重

练习 5(5 分钟):修复报错
- 输入:以下代码报错,找出原因并修复

class A:
def __init__(self, v):
    self.v = v
def __eq__(self, other):
    return self.v == other.v
a1 = A(1)
a2 = A(1)
print({a1, a2})  # 报错:unhashable type
  • 预期输出:{A(1)}(只有一个元素)
  • 提示:加了 __eq__ 就必须加 __hash__

作业:做一个「个人账本工具」

  • 需求描述:记录你的日常收入和支出,支持按月统计,显示结余
  • 功能点
    1. 用类方法 from_entry 创建账目条目
    2. 用 __str__ 美化输出(显示「💰 收入 +100元」或「💸 支出 -50元」)
    3. 用静态方法 is_valid_amount 验证金额(正数才合法)
    4. 支持保存到 JSON 文件
  • 加分项
    1. 按月份筛选账目
    2. 用 __repr__ 显示月度统计摘要
  • 验收标准:能跑起来、能添加账目、能显示月度结余
  • 提交方式:评论区贴代码

📚 总结

这一章学了三个让对象更好用的工具:
1. @classmethod —— 类的另类构造函数,批量创建对象超方便
2. @staticmethod —— 挂靠在类名下的工具函数,不依赖实例
3. 魔术方法 —— __str__/__repr__/__eq__/__hash__,让对象更聪明

延伸资源
- 官方文档:Data Model - Python(第 3 章讲的就是这些)
- 书籍:《Python 编程:从入门到实践》第 9 章
- 视频:搜索「Python 魔术方法」有大量可视化教程

互动钩子:你在写代码时遇到过 __eq____hash__ 不一致的坑吗?或者用过什么有趣的类方法/静态方法?评论区聊聊,老粉优先回复!


下章预告:学完了类方法、静态方法、魔术方法,是时候做一个完整的银行账户管理系统了。下一章我们会综合运用这 4 章的所有知识,从类设计到继承,从多态到这些方法,写出一个能存钱、取钱、转账、带利息计算的完整系统。敬请期待!

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