python 进阶(三):面向对象进阶——从“会写 class”到设计可维护的后端对象

一、前言

前面的学习中,我们已经接触过:

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、依赖注入、回调处理器中,经常会看到"对象可以像函数一样使用"的设计。

十二、类方法与静态方法

  1. @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
  1. @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 是动态语言,项目越来越大后,如何让函数参数、返回值、数据结构和接口契约更加清楚?

答案就是类型标注。

相关推荐
用户0332126663672 小时前
Python 办公自动化:用 Python 快速生成精美 PPT 演示文稿
python
打工仔折腾 AI2 小时前
极空间部署Portainer实战:绕过Socket权限、解决9000端口冲突与固定内网访问
人工智能·后端·python·性能优化
魔都吴所谓2 小时前
【Python】 多线程 & 多进程&协程 常见题 + 应用场景整理
开发语言·python
Data_Journal2 小时前
如何使用 Python 解析 HTML
python·html
king13598745502 小时前
LiteRT Tensor API:C++ 编写端侧模型图,XNNPACK、GPU 与 NPU 怎么选?
开发语言·c++·人工智能·chatgpt·架构·aigc
geovindu2 小时前
rust: Template Method Pattern(续)
开发语言·后端·设计模式·rust·行为模式·板方法模式
外收内放2 小时前
面向对象高级(封装)
开发语言·python
全栈练习生2 小时前
大模型原理之投机解码
python·ai
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:差分数组与区间修改实战解析
c++·python·算法·bfs·信息学竞赛