一、前言
前面的学习中,我们已经接触过:
python
class User:
...
也知道可以通过:user = User(...)创建对象。
但在真实项目中,类不只是"把几个变量放到一起"。它更重要的价值是:
把有关联的数据和行为组织在一起
明确不同对象的职责
控制数据的修改方式
让项目可以持续扩展,而不变得混乱
例如一个任务管理后端中,通常不只有一个 Task 类,而可能有:
Task → 任务本身
TaskRepository → 负责数据库读写
TaskService → 负责业务规则
TaskNotifier → 负责发送通知
User → 用户实体
这一篇重点学习:
继承与 super()
多态
抽象类 ABC
组合优于继承
@dataclass
@property
类方法与静态方法
常见魔术方法
二、从普通类开始:对象为什么适合表示业务实体
假设我们用一个类表示任务:
python
class Task:
def __init__(self, title, owner_id):
self.title = title
self.owner_id = owner_id
self.done = False
创建任务:
python
task = Task(
title="学习 FastAPI",
owner_id=1,
)
print(task.title)
print(task.done)
输出:
python
学习 FastAPI
False
这里的Task,是类,表示任务的设计图。task是对象,表示一条具体任务。self代表当前创建出来的对象本身。因此:self.title = title表示把传入的 title 保存到当前对象的 title 属性中。
如果创建两条任务:
task_1 = Task("学习 FastAPI", 1)
task_2 = Task("学习 Docker", 2)
它们会有各自独立的数据:
print(task_1.title)
print(task_2.title)
输出:
学习 FastAPI
学习 Docker
三、让类不仅保存数据,也保存行为
任务不应该只保存标题和状态,它还应该拥有"完成任务""重新打开任务"等行为。
python
class Task:
def __init__(self, title, owner_id):
self.title = title
self.owner_id = owner_id
self.done = False
def complete(self):
self.done = True
def reopen(self):
self.done = False
使用:
python
task = Task("学习 SQLAlchemy", 1)
task.complete()
print(task.done)
输出:
python
True
这里:task.complete()本质上相当于:Task.complete(task),Python 会自动把当前对象 task 传给方法的第一个参数 self。这就是为什么实例方法第一个参数通常叫:seif。
四、@property:不要让外部随意破坏对象状态
假设任务标题不能为空。如果我们直接允许:
task.title = ""
那么业务数据可能变得不合法。
可以通过 **@property**控制属性修改:
python
class Task:
def __init__(self, title, owner_id):
self.title = title
self.owner_id = owner_id
self.done = False
@property
def title(self):
return self._title
@title.setter
def title(self, value):
value = value.strip()
if not value:
raise ValueError("任务标题不能为空")
if len(value) > 100:
raise ValueError("任务标题不能超过 100 个字符")
self._title = value
使用:
python
task = Task("学习 FastAPI", 1)
task.title = "学习 JWT"
print(task.title)
输出:学习 JWT,如果写:task.title = " "会得到:ValueError: 任务标题不能为空。这里的@property让你可以像访问普通属性一样写:task.title,但底层其实调用了:
python
def title(self):
...
而:
python
@title.setter
会在你写:
python
task.title = "新标题"
时执行校验逻辑。这体现了一个重要思想:
对象不应该把所有内部数据完全暴露给外部,而应该由自己保证数据始终合法。
五、继承:让子类复用父类能力
假设系统有不同类型的用户:
普通用户
管理员
VIP 用户
它们都拥有:
用户名
用户 ID
登录行
可以先定义父类:
python
class User:
def __init__(self, user_id, username):
self.user_id = user_id
self.username = username
def get_profile(self):
return {
"user_id": self.user_id,
"username": self.username,
}
再定义管理员:
python
class AdminUser(User):
def delete_task(self, task_id):
return f"管理员 {self.username} 删除了任务 {task_id}"
创建对象:
python
admin = AdminUser(
user_id=1,
username="admin",
)
print(admin.get_profile())
print(admin.delete_task(10))
输出:
python
{'user_id': 1, 'username': 'admin'}
管理员 admin 删除了任务 10
这里:
python
class AdminUser(User):
表示:
AdminUser继承User。
所以管理员既拥有父类的:
python
get_profile()
也拥有自己的:
python
delete_task()
5.1 super():调用父类初始化逻辑
如果管理员还需要角色信息,可以这样写:
python
class AdminUser(User):
def __init__(self, user_id, username, role="admin"):
super().__init__(user_id, username)
self.role = role
def delete_task(self, task_id):
return f"管理员 {self.username} 删除了任务 {task_id}"
其中:
super().__init__(user_id, username)
表示调用父类 User 的初始化方法。
它相当于:
python
User.__init__(self, user_id, username)
但推荐使用:
super()
因为它更适合复杂继承关系,也更符合现代 Python 的写法。
python
admin = AdminUser(
user_id=1,
username="admin",
)
print(admin.username)
print(admin.role)
输出:
python
admin
admin
六. 方法重写与多态
重写(override) :子类重新实现父类已经定义的同名方法。多态(polymorphism) :同一个调用代码,传入不同子类实例,自动执行子类重写后的逻辑,父类有时会定义一个通用行为,但不同子类需要不同实现。例如通知系统。先定义父类:
python
class Notifier:
def send(self, message):
raise NotImplementedError("子类必须实现 send 方法")
邮件通知:
python
class EmailNotifier(Notifier):
def send(self, message):
print(f"发送邮件通知:{message}")
短信通知:
python
class SMSNotifier(Notifier):
def send(self, message):
print(f"发送短信通知:{message}")
在业务代码中:
python
def notify_task_completed(notifier, task_title):
notifier.send(f"任务已完成:{task_title}")
调用:
python
email_notifier = EmailNotifier()
sms_notifier = SMSNotifier()
notify_task_completed(
email_notifier,
"学习 FastAPI",
)
notify_task_completed(
sms_notifier,
"学习 Docker",
)
输出:
发送邮件通知:任务已完成:学习 FastAPI
发送短信通知:任务已完成:学习 Docker
这里:
notify_task_completed()
不关心传入的是邮件通知还是短信通知。它只要求这个对象拥有:send(message)方法。这就是多态。因为子类重写 了方法,才能够实现多态 。 **重写是前提,多态是运行时表现出来的结果。**可以把多态理解为:
调用方式相同,但不同对象会执行各自不同的实现。
真实项目中,多态常见于:
不同支付方式:支付宝、微信、银行卡
不同通知方式:邮件、短信、站内信
不同大模型提供商:OpenAI、通义千问、DeepSeek
不同文件解析器:PDF、Word、Markdown
不同向量数据库:Chroma、FAISS、Milvus
七、抽象类 ABC:提前规定"必须实现什么能力"
刚才的 Notifier 使用:
python
raise NotImplementedError(...)
提醒子类实现 send()。Python 更标准的做法是使用抽象类。
python
from abc import ABC, abstractmethod
定义抽象通知类:
python
class Notifier(ABC):
@abstractmethod
def send(self, message):
pass
邮件通知实现:
python
class EmailNotifier(Notifier):
def send(self, message):
print(f"发送邮件:{message}")
短信通知实现:
python
class SMSNotifier(Notifier):
def send(self, message):
print(f"发送短信:{message}")
这里的ABC表示抽象基类。@abstractmethod表示抽象方法。如果子类没有实现send(),就不能正常创建对象。
例如:
python
class WeChatNotifier(Notifier):
pass
再执行:
notifier = WeChatNotifier()
会报错,因为它没有实现必须的方法。
抽象类的意义是:
给系统中的一类对象定义统一接口,避免不同实现随意发挥。
八、组合优于继承
初学面向对象时,容易把所有关系都写成继承。例如错误地设计:
python
class TaskService(Task):
...
这不合适。因为:
TaskService 不是一种 Task
它只是"使用 Task 的服务"。一般来讲使用继承关系是在一定程度上面帮助我们减少代码的重复度并且能够让类之间有一定的关系,但并不是所有的关系都需要写成继承,对于上述举出的例子,这时应该使用组合。
例如:
python
class TaskService:
def __init__(self, notifier):
self.notifier = notifier
def complete_task(self, task):
task.complete()
self.notifier.send(
f"任务完成:{task.title}"
)
使用:
python
task = Task("学习 Python 面向对象", 1)
notifier = EmailNotifier()
service = TaskService(notifier)
service.complete_task(task)
输出:
python
发送邮件:任务完成:学习 Python 面向对象
关系是:
TaskService 拥有一个 Notifier
TaskService 操作一个 Task
而不是:
TaskService 是一种 Task
这是非常重要的工程设计思想。
通常:A是一种B才考虑继承。
例如:
AdminUser 是一种 User
EmailNotifier 是一种 Notifier
而:
TaskService 使用 Notifier
TaskService 使用 TaskRepository
TaskService 使用 LLMClient
更适合组合。
九、@dataclass:减少纯数据类的样板代码
有些类主要用于保存数据,并不需要写很多复杂方法。例如聊天消息:
python
class Message:
def __init__(self, role, content, session_id):
self.role = role
self.content = content
self.session_id = session_id
可以使用:
python
from dataclasses import dataclass
简化:
python
@dataclass
class Message:
role: str
content: str
session_id: int
创建对象:
python
message = Message(
role="user",
content="什么是 JWT?",
session_id=1,
)
print(message)
输出类似:
python
Message(role='user', content='什么是 JWT?', session_id=1)
@dataclass 会自动帮助我们生成常用方法,例如:
python
__init__
__repr__
__eq__
因此:
python
message_1 = Message("user", "你好", 1)
message_2 = Message("user", "你好", 1)
print(message_1 == message_2)
输出:
True
普通类默认比较的是不是同一个对象,而 dataclass 会比较字段值。
十、default_factory:避免可变默认值陷阱
看下面这个错误写法:
python
from dataclasses import dataclass
@dataclass
class ChatSession:
session_id: int
messages: list = []
这里的:
messages = []
会被多个对象共享,容易造成严重问题。Python 在加载定义类代码那一刻 ,就把[]这个列表创建好了。之后每次新建实例,默认用的都是同一个 list 对象 ,不是每次新建空列表。这个坑根源:函数定义阶段就创建可变默认对象,后续调用全部复用这个对象 。 修复方案:默认值用None,函数内部再实例化 list/dict;dataclass 里面就用field(default_factory=list)。
正确写法:
python
from dataclasses import dataclass, field
@dataclass
class ChatSession:
session_id: int
messages: list = field(default_factory=list)
field是 dataclasses 模块提供的函数。当我们需要对 dataclass 字段做精细化配置的时候使用。最常用的是default_factory,用来给 list、dict 这类可变类型生成默认值,避免多个实例共享同一个可变对象的陷阱,现在每个 ChatSession 都会拥有自己的空列表。
python
session_1 = ChatSession(1)
session_2 = ChatSession(2)
session_1.messages.append("用户 1 的消息")
print(session_1.messages)
print(session_2.messages)
输出:
['用户 1 的消息']
[]
十一、常见魔术方法
魔术方法通常以双下划线开头和结尾:
__init__
__repr__
__str__
__eq__
__len__
__call__
11.1 __repr__用于对象调试时的显示。
python
class Task:
def __init__(self, task_id, title, done=False):
self.task_id = task_id
self.title = title
self.done = done
def __repr__(self):
return (
f"Task("
f"id={self.task_id}, "
f"title={self.title!r}, "
f"done={self.done}"
f")"
)
现在:
python
task = Task(1, "学习 FastAPI")
print(task)
输出:
Task(id=1, title='学习 FastAPI', done=False)
这会让日志、调试器、终端输出更容易阅读。
11.2 len
__len__让对象支持:
len(obj)
例如任务列表:
python
class TaskList:
def __init__(self, tasks):
self.tasks = tasks
def __len__(self):
return len(self.tasks)
使用:
python
task_list = TaskList([
Task(1, "学习 FastAPI"),
Task(2, "学习 MySQL"),
])
print(len(task_list))
输出:
2
11.3 call
让对象像函数一样被调用。
python
class TaskValidator:
def __call__(self, title):
title = title.strip()
if not title:
raise ValueError("任务标题不能为空")
return title
使用:
python
validator = TaskValidator()
title = validator("学习 Docker")
print(title)
这里:
python
validator("学习 Docker")
本质上会调用:
python
validator.__call__("学习 Docker")
这个__call__的作用就是可以让对象像函数一样去调用,但是这个是有什么作用呢?首先,普通函数很难保存状态 ;而 __call__ 让对象能像函数一样调用,同时还可以保存自己的内部状态、配置 。兼顾「函数简洁的调用语法」+「类可以存属性的能力」。它最大的价值:可以把这个对象,当成参数传给其他接收「函数」的代码。在 FastAPI、依赖注入、回调处理器中,经常会看到"对象可以像函数一样使用"的设计。
十二、类方法与静态方法
@classmethod
类方法第一个参数通常是:
cls
它代表类本身。而不像self是代表实例对象,可以类直接调用,也可以实例调用 ,可以读取 / 修改类属性 ,不能直接访问实例属性。
python
class User:
def __init__(self, name):
self.name = name
@classmethod
def from_full_str(cls, s):
# 从字符串构造实例
return cls(s)
u = User.from_full_str("张三")
print(u.name)
例如也可以通过不同来源创建用户:
python
class User:
def __init__(self, username, role="user"):
self.username = username
self.role = role
@classmethod
def from_dict(cls, data): //这个cls就代表的是类的本身
return cls(
username=data["username"],
role=data.get("role", "user"),
)
使用:
python
data = {
"username": "kankan",
"role": "admin",
}
user = User.from_dict(data)
print(user.username)
print(user.role)
输出:
kankan
admin
@staticmethod
静态方法不需要:self,也不需要:cls。本质:只是一个放在类内部的普通独立函数,类调用 / 实例调用都可以,不能直接访问实例属性,也不能直接访问类属性。
它只是逻辑上和类相关的工具函数。
python
class PasswordValidator:
@staticmethod
def validate(password):
if len(password) < 6:
raise ValueError("密码至少需要 6 位")
return True
使用:
PasswordValidator.validate("123456")
十三、真实后端中不同类的职责
在真实 FastAPI 项目中,建议逐渐形成下面的认识:
SQLAlchemy Model
↓
负责数据库表结构与 ORM 映射
Pydantic Schema
↓
负责接口输入与输出数据校验
Service
↓
负责业务规则
Repository
↓
负责数据库查询与保存
Client
↓
负责调用外部服务,例如大模型、Redis、支付接口
Domain Object / dataclass
↓
负责表达项目中的业务数据
例如未来一个 AI 对话项目可能有:
User 用户数据
Conversation 会话数据
Message 消息数据
Document 文档数据
ChatService 对话业务逻辑
RAGService 文档检索逻辑
LLMClient 大模型调用逻辑
MessageRepository 消息数据库操作
不需要一开始就拆这么多文件,但当项目开始增长时,按职责拆分会比把所有代码塞进 main.py 更容易维护。
十四、本文总结
这一篇最重要的结论是:
类不是为了"显得高级"
而是为了让数据、规则和行为有清晰归属
请记住下面几个判断:
A 是一种 B
→ 考虑继承
A 使用 B
→ 更适合组合
对象的数据不能被随意修改
→ 使用 @property 做校验
一组不同对象必须遵守相同接口
→ 使用抽象类 ABC
类主要保存数据
→ 考虑 @dataclass
下一篇将继续解决一个真实工程问题:
Python 是动态语言,项目越来越大后,如何让函数参数、返回值、数据结构和接口契约更加清楚?
答案就是类型标注。