第9章 9.4 dataclass 与 attrs:让数据类自己搞定繁琐代码
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了用 with 语句管理资源,现在你写代码时已经能优雅地处理「打开→关闭」「加锁→释放」这类配对操作了。
但你有没有遇到过这种烦心事:
痛点1:你要写一个「学生信息」类,光是为了存储姓名、年龄、成绩,你得手动写 __init__、__repr__、__eq__ 一堆模板代码,核心业务还没开始写呢,重复劳动先做半小时。
痛点2:你写了 5 个类,发现每个类都有类似的「打印调试信息」需求,改一行逻辑要改 5 个地方,心累。
痛点3:你想给类加个默认值、想校验输入、想让某个字段不可变——手写的话代码又臭又长。
学完这一章,上面三个痛点将统统消失。你会学到两把利器:dataclass(Python 内置,轻量)和 attrs(第三方库,功能更全)。它们的核心思想是一样的:用装饰器自动生成那些重复的模板代码,让你专注写真正的业务逻辑。
🧱 基础 25 分钟:核心概念
什么是 dataclass?—— 包装盒的自动化工厂
想象你开了一家快递站,每天要打包 1000 个包裹。每个包裹都要经历:装货 → 贴标签 → 封箱 → 记录物流。如果这些步骤全靠人工,一模一样的流程要重复 1000 遍。
dataclass 就是这个自动化工厂——你只要告诉它「我要一个装水果的盒子」,它自动帮你把装货、贴标签、封箱的流程跑完,你只需要往里面放水果就行。
生活类比:dataclass = 给你一个自带说明书、自带比较功能、自带打印格式的数据盒子
为什么要用 dataclass?
你可能会问:我用普通类也能写啊,干嘛非要用 dataclass?
看这个对比:
# 普通类写法 - 要手写一堆
class Student:
def __init__(self, name: str, age: int, score: float):
self.name = name
self.age = age
self.score = score
def __repr__(self):
return f"Student(name={self.name}, age={self.age}, score={self.score})"
def __eq__(self, other):
if not isinstance(other, Student):
return False
return self.name == other.name and self.age == other.age and self.score == other.score
# dataclass 写法 - 5 行搞定
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
score: float
两段代码功能几乎一样,但第二段你只需要专注「name、age、score 是啥」,__init__、__repr__、__eq__ 全都自动生成了。
第一个 dataclass:正确打开方式
来,上手跑一个:
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
price: float
in_stock: bool = True # 有默认值,默认在库
# 创建实例
book1 = Book("活着", "余华", 39.9)
book2 = Book("活着", "余华", 39.9)
print(book1)
print(book1 == book2)
输出:
Book(title='活着', author='余华', price=39.9, in_stock=True)
True
解释:
- @dataclass 装饰器自动生成了 __init__(构造)、__repr__(打印)、__eq__(比较)
- in_stock: bool = True 这个参数有默认值,创建时可以不传
- 两个字段完全相同的 Book 实例,== 比较返回 True
这就是 dataclass 的核心能力——写更少的代码,做更多的事。

dataclass 进阶:field 和默认值工厂
有时候你不想所有实例共享同一个可变对象(比如列表),这时候要用 field(default_factory=...):
from dataclasses import dataclass, field
@dataclass
class ShoppingCart:
owner: str
items: list = field(default_factory=list) # 每个实例独立的空列表
total: float = 0.0
# 测试
cart1 = ShoppingCart("小明")
cart2 = ShoppingCart("小红")
cart1.items.append("苹果")
print(f"cart1: {cart1}")
print(f"cart2: {cart2}")
输出:
cart1: ShoppingCart(owner='小明', items=['苹果'], total=0.0)
cart2: ShoppingCart(owner='小红', items=[], total=0.0)
解释:
- 如果用 items: list = [](直接赋空列表),所有实例会共享同一个列表(坑!)
- 用 field(default_factory=list) 确保每个实例有自己独立的空列表
dataclass 继承:子类还能自动继承
from dataclasses import dataclass, field
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str = "中华田园犬"
tricks: list = field(default_factory=list)
# 测试
dog = Dog(name="旺财", age=3, breed="金毛", tricks=["坐下", "握手"])
print(dog)
输出:
Dog(name='旺财', age=3, breed='金毛', tricks=['坐下', '握手'])
解释:子类继承父类,子类的字段和父类的字段会合并生成一个新的 __init__。
对比 namedtuple:谁是更好的选择?
Python 还有一个叫 namedtuple 的东西,长得和 dataclass 有点像:
from collections import namedtuple
# namedtuple 写法
Point = namedtuple('Point', ['x', 'y'])
p1 = Point(1, 2)
print(p1.x, p1.y, p1)
# dataclass 写法
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p2 = Point(1, 2)
print(p2.x, p2.y, p2)
两者都能用 .x 访问属性,都能打印。但区别在哪?
| 特性 | namedtuple | dataclass |
|---|---|---|
| 可变字段 | ❌ 不支持 | ✅ 支持 |
| 默认值 | ✅ 支持 | ✅ 支持 |
| 方法自定义 | ❌ 只能继承 | ✅ 可以自己写方法 |
| 类型提示 | ❌ 没有 | ✅ 有 |
| 继承 | ❌ 不支持 | ✅ 支持 |
一句话总结:namedtuple 是轻量级不可变数据容器,dataclass 是功能更全的数据类。
什么是 attrs?—— dataclass 的功能加强版
如果说 dataclass 是基础款洗衣机,那 attrs 就是顶配版——烘干、杀菌、静音全给你加上。
attrs 是个第三方库,需要先安装:
pip install attrs
核心用法:
import attr
@attr.s
class Student:
name = attr.ib()
age = attr.ib()
score = attr.ib(default=0.0)
stu = Student("张三", 20)
print(stu)
输出:
Student(name='张三', age=20, score=0.0)
attrs 比 dataclass 多了什么?
- 更灵活的验证器(@validators)
- 更细致的转换器(@converters)
- 完整的 slots 支持(性能更好,内存占用更小)

🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):学生成绩管理器
跟着抄就能跑,学会 dataclass 的基本操作:
from dataclasses import dataclass
from typing import Optional
@dataclass
class StudentRecord:
name: str
chinese: float
math: float
english: float
student_id: str = "未分配"
@property
def average(self) -> float:
return (self.chinese + self.math + self.english) / 3
@property
def total(self) -> float:
return self.chinese + self.math + self.english
def grade(self) -> str:
avg = self.average
if avg >= 90:
return "A"
elif avg >= 80:
return "B"
elif avg >= 70:
return "C"
elif avg >= 60:
return "D"
else:
return "F"
# 创建两条记录
stu1 = StudentRecord("小明", 85.0, 92.0, 88.0, "S001")
stu2 = StudentRecord("小红", 76.0, 81.0, 69.0, "S002")
print(f"{stu1.name} - 总分: {stu1.total}, 平均: {stu1.average:.1f}, 等级: {stu1.grade()}")
print(f"{stu2.name} - 总分: {stu2.total}, 平均: {stu2.average:.1f}, 等级: {stu2.grade()}")
# 比一比谁成绩更好
print(f"两人成绩相同吗?{stu1 == stu2}")
预期输出:
小明 - 总分: 265.0, 平均: 88.3, 等级: B
小红 - 总分: 226.0, 平均: 75.3, 等级: C
两人成绩相同吗?False
解释:这个例子展示了 dataclass 可以加方法(average、total、grade),还能自动比较相等性。
项目 2(15 分钟):从 CSV 文件读取商品数据并统计
现在你有一个 products.csv 文件,内容如下:
name,price,category,stock
iPhone 15,6999,手机,100
MacBook Pro,14999,电脑,50
AirPods,1299,配件,200
iPad Air,4799,平板,80
Apple Watch,2999,配件,150
我们要用 dataclass 建模商品,然后用它统计各种信息:
from dataclasses import dataclass, field
from typing import List
import csv
@dataclass
class Product:
name: str
price: float
category: str
stock: int
@property
def total_value(self) -> float:
return self.price * self.stock
def load_products(filepath: str) -> List[Product]:
products = []
with open(filepath, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
products.append(Product(
name=row['name'],
price=float(row['price']),
category=row['category'],
stock=int(row['stock'])
))
return products
# 加载数据
products = load_products('products.csv')
# 统计各类商品库存总值
from collections import defaultdict
category_value = defaultdict(float)
for p in products:
category_value[p.category] += p.total_value
print("=== 各品类库存总值 ===")
for cat, value in category_value.items():
print(f"{cat}: ¥{value:,.2f}")
# 找出库存最少的商品
min_stock_product = min(products, key=lambda p: p.stock)
print(f"\n库存最少: {min_stock_product.name}, 仅剩 {min_stock_product.stock} 件")
# 计算所有商品的总库存价值
total_inventory_value = sum(p.total_value for p in products)
print(f"总库存价值: ¥{total_inventory_value:,.2f}")
预期输出(假设 products.csv 在当前目录):
=== 各品类库存总值 ===
手机: ¥699,900.00
电脑: ¥749,950.00
配件: ¥454,350.00
平板: ¥383,920.00
库存最少: MacBook Pro, 仅剩 50 件
总库存价值: ¥2,288,120.00
解释:用 dataclass 定义数据结构后,读 CSV、处理数据、统计分析都能优雅地链在一起。
项目 3(15 分钟):做一个命令行待办清单小工具
组合项目 1 和项目 2 的能力,做一个真实的命令行工具:
from dataclasses import dataclass, field
from typing import List
import json
import os
TASK_FILE = "tasks.json"
@dataclass
class Task:
title: str
priority: int # 1-5,数字越大越重要
done: bool = False
created_at: str = ""
def mark_done(self):
self.done = True
def __str__(self):
status = "✅" if self.done else "⬜"
return f"{status} [{self.priority}] {self.title}"
@dataclass
class TodoList:
name: str
tasks: List[Task] = field(default_factory=list)
def add_task(self, title: str, priority: int = 3):
task = Task(title=title, priority=priority)
self.tasks.append(task)
print(f"已添加: {task}")
def complete_task(self, index: int) -> bool:
if 0 <= index < len(self.tasks):
self.tasks[index].mark_done()
return True
return False
def show_pending(self):
pending = [t for t in self.tasks if not t.done]
if not pending:
print("🎉 所有任务都完成了!")
return
# 按优先级排序
pending.sort(key=lambda t: t.priority, reverse=True)
print(f"=== {self.name} 待办事项 ({len(pending)} 项) ===")
for i, task in enumerate(pending):
print(f" {i+1}. {task}")
def save(self):
data = {
"name": self.name,
"tasks": [
{"title": t.title, "priority": t.priority, "done": t.done}
for t in self.tasks
]
}
with open(TASK_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False)
print(f"已保存到 {TASK_FILE}")
@classmethod
def load(cls) -> 'TodoList':
if not os.path.exists(TASK_FILE):
return TodoList(name="我的待办")
with open(TASK_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
tasks = [Task(**t) for t in data["tasks"]]
return cls(name=data["name"], tasks=tasks)
# 演示用法
if __name__ == "__main__":
todo = TodoList.load()
# 添加几个任务
todo.add_task("写完 Python 作业", priority=5)
todo.add_task("整理房间", priority=2)
todo.add_task("给妈妈打电话", priority=4)
# 展示待办
todo.show_pending()
# 完成一个任务
todo.complete_task(0)
todo.show_pending()
# 保存
todo.save()
预期输出:
已添加: ⬜ [5] 写完 Python 作业
已添加: ⬜ [2] 整理房间
已添加: ⬜ [4] 给妈妈打电话
=== 我的待办事项 (3 项) ===
1. ⬜ [5] 写完 Python 作业
2. ⬜ [4] 给妈妈打电话
3. ⬜ [2] 整理房间
=== 我的待办事项 (2 项) ===
1. ⬜ [4] 给妈妈打电话
2. ⬜ [2] 整理房间
已保存到 tasks.json
解释:这个工具用 dataclass 优雅地组织了任务数据,还支持持久化存储(JSON 文件),可以当作你的日常小助手。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:可变默认值要小心
# ❌ 错误写法 - 所有实例共享同一个列表
@dataclass
class Wrong:
items: list = []
# ✅ 正确写法 - 每个实例独立
@dataclass
class Right:
items: list = field(default_factory=list)
原因:dataclass 的默认值在类定义时创建,如果用可变对象,所有实例会共享它。
坑 2:字段顺序影响构造函数
# ❌ 错误 - 默认值字段不能在无默认值字段前面
@dataclass
class Wrong:
name: str = "匿名" # 有默认值
age: int # 没有默认值,错误!
# ✅ 正确 - 无默认值字段在前,有默认值的在后
@dataclass
class Right:
age: int
name: str = "匿名"
坑 3:dataclass 生成的方法可能被你的方法覆盖
@dataclass
class MyClass:
x: int
# ❌ 你自己定义了 __repr__,会覆盖 dataclass 生成的
def __repr__(self):
return f"<MyClass {self.x}>"
# ✅ 如果你想扩展,可以调用 dataclass 生成的
def detailed_repr(self):
return f"详细: {self.__repr__()}"
坑 4:继承时字段顺序有讲究
@dataclass
class Base:
a: int
@dataclass
class Child(Base):
b: int
a: int = 10 # ❌ 子类不能重新定义父类已有字段
坑 5:dataclass 不做类型校验
@dataclass
class Person:
name: str
age: int
# ❌ 类型提示只是提示,Python 不会阻止你传入错误类型
p = Person(name=123, age="不是数字") # 能运行,但逻辑上不对
如果你想加类型校验,可以用 attrs 的验证器:
import attr
@attr.s
class Person:
name = attr.ib(type=str)
age = attr.ib(type=int, validator=attr.validators.instance_of(int))
性能小优化:使用 slots=True
dataclass 默认用字典存储属性,如果你有大量实例,可以用 slots=True 节省内存:
@dataclass(slots=True)
class Optimized:
x: int
y: int
注意:slots=True 的类不能使用可变默认值(field(default_factory=list) 仍可用)。
调试技巧:用 field(repr=False) 隐藏敏感字段
@dataclass
class User:
username: str
password: str = field(repr=False) # 打印时不显示密码
email: str
user = User("张三", "123456", "zhangsan@example.com")
print(user)
输出:User(username='张三', email='zhangsan@example.com')(密码被隐藏了)
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):抄改基础题
- 输入:基于项目 1 的 StudentRecord,把小明换成你自己,填入三科成绩
- 预期输出:打印你的总分和平均分
- 提示:只改构造函数的参数值
练习 2(2 分钟):加个判断
- 输入:在练习 1 的基础上,加一个 if 判断,如果平均分 >= 90 打印「学霸!」
- 预期输出:成绩好的打印「学霸!」,否则打印「继续加油」
- 提示:用 if stu1.average >= 90: 判断
练习 3(2 分钟):用新数据处理
- 输入:创建一个 products.csv,包含你喜欢的 3 本书(name, price, category, stock)
- 预期输出:用项目 2 的代码统计你的「书库」库存总值
- 提示:CSV 的列名要和代码里的一致(name, price, category, stock)
练习 4(2 分钟):串起两个项目
- 输入:用项目 2 的 Product dataclass 设计一个「书架」的数据结构
- 预期输出:打印每本书的库存价值
- 提示:把 Product 的 total_value 思路用过来
练习 5(2 分钟):报错分析
- 输入:运行下面这段「有问题」的代码,观察报错
from dataclasses import dataclass
@dataclass
class Broken:
name: str = "默认名"
value: int # ❌ 没有默认值但在有默认值的后面
obj = Broken(100) # 这行会报错
- 预期输出:你能说出为什么报错,以及怎么修复
- 提示:Python 报错信息里有答案
作业题(30 分钟 - 2 小时)
作业:做一个「课程表 + 成绩记录」小工具
- 需求描述:做一个命令行工具,能管理你的课程表和每门课的成绩
- 功能点:
1. 用 dataclass 定义Course(课程)和Grade(成绩)两个数据类
2. 课程包含:课程名、老师、上课时间(周几 + 第几节)、教室
3. 成绩包含:课程名、平时分、期末分、最终成绩(加权计算)
4. 支持添加课程、添加成绩、查看所有课程、查看 GPA - 加分项:
1. 数据持久化到 JSON 文件(下次打开还在)
2. 用field(repr=False)隐藏某个敏感字段 - 验收标准:能跑起来 + 输入输出符合预期 + 代码有注释
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章核心 3 点
@dataclass是自动生成模板代码的装饰器:用了它,__init__、__repr__、__eq__全部自动搞定- 可变默认值要用
field(default_factory=...):避免所有实例共享同一个列表/字典 attrs是 dataclass 的功能加强版:如果 dataclass 不够用,试试 attrs 的验证器和slots优化
延伸资源
- Python dataclasses 官方文档
- attrs 官方文档
- 《Python Crash Course》第 9 章:类(配合 dataclass 一起学)
互动钩子
你有没有被「要写一堆
__init__和__repr__」折磨过的经历?或者你现在在用什么方法解决这个问题?评论区聊聊,老粉优先回复!下一章我们要学「项目结构与代码组织」,学了之后你就能把这些散落的 dataclass 代码组织成一个真正的 Python 项目了。
往期章节回顾:
- 第 9 章 9.3 上下文管理器 with 进阶 - 学会了 with 语句优雅管理资源
- 第 10 章 10.1 项目结构与代码组织 - 预告:如何把 dataclass 们放进一个正经项目里

评论(0)