仍然需要多读几遍,读一遍,一个小时又过去了
核心定位
本篇面向 JavaScript / TypeScript 开发者,彻底理清 Python 的类、对象、继承机制与数据模型。
在 FastAPI 等现代化后端框架中,面向对象不再是单纯的“画类图”,而是由多种职责不同的类配合协作。本篇先建立 Python OOP 的完整底座,最后收口到 FastAPI 的类职责拆解与依赖注入。
后端请求链路中各种“类”的职责分工
一次 HTTP 请求处理中,不同类型的“类”各自守住一道边界:
- 请求模型(Pydantic
BaseModel)- 守住输入边界
- 负责外部 JSON 数据的校验与类型转换
- 服务类(Service Class)
- 封装核心业务行为
- 管理业务状态与多资源协作
- ORM 类(SQLAlchemy / SQLModel)
- 映射数据库表结构
- 将单行记录转化为 Python 实体对象
- 响应模型(Pydantic
BaseModel)- 决定哪些字段可以离开服务端
- 过滤敏感数据并序列化输出
定义类、创建对象与 self
Python
class Task:
# __init__ 在实例创建后初始化这个实例
def __init__(self, task_id: int, title: str) -> None:
# self.task_id 和 self.title 都是实例属性
self.task_id = task_id
self.title = title
self.done = False
# 实例方法的第一个参数必须显式接收当前实例
def complete(self) -> None:
self.done = True
# 调用类,得到 Task 实例
task = Task(task_id=1, title="学习 Python 类")
task.complete()
print(task.title) # 学习 Python 类
print(task.done) # True
self就是当前实例。定义方法时必须显式写出来,调用task.complete()时 Python 会自动把task传给self。
task.complete() 可以直接理解为:
# 两种调用的实际效果相同
task.complete()
Task.complete(task)TypeScript
class Task {
done = false;
constructor(
public taskId: number,
public title: string,
) {}
complete(): void {
// this 由方法调用方式决定,不写进形参列表
this.done = true;
}
}
const task = new Task(1, "学习 Python 类");
task.complete();补充
- Python 创建实例不写
new,直接调用Task(...)。 - Python 用缩进形成类体,不用
{}。 - Python 方法必须显式写
self;JS/TS 的this不出现在普通形参列表里。 self只是约定俗成的名字,不是关键字,但不要改成别的名字。- Python 取方法时会得到绑定方法,因此
callback = task.complete后再调用callback(),仍然知道实例是task。- JS 把
const callback = task.complete单独传走后,this可能丢失;- 常见处理是
task.complete.bind(task)或箭头函数。
- 常见处理是
- JS 把
__init__ 是初始化器,不是完整的构造过程
日常开发可以暂时把 __init__ 当 constructor 用,但机制上要知道:
__new__:- 先创建并返回实例。
__init__:- 再接收已经创建好的实例并初始化它。
__init__必须返回None,不能返回另一个对象。
class Task:
def __new__(cls, task_id: int, title: str):
# cls 是当前要创建实例的类
instance = super().__new__(cls)
print("先创建实例")
return instance
def __init__(self, task_id: int, title: str) -> None:
# 再把数据写入已经创建好的实例
print("再初始化实例")
self.task_id = task_id
self.title = title普通业务类只写
__init__。只有不可变类型、自定义实例创建、缓存实例等少数场景才需要碰__new__。
和 TS 对照:
- TS 的
constructor是语言提供的构造入口。 - Python 的
Task(...)先经过类对象的调用机制,再进入__new__→__init__。 - 不要在 Python 里为了“像构造函数”而主动调用
task.__init__(...);- 需要新对象就重新调用类。
实例属性与类属性
Python
class Task:
# 类属性:所有实例通过同一个类读取
category = "todo"
def __init__(self, title: str) -> None:
# 实例属性:每个实例各自保存
self.title = title
self.tags: list[str] = []
first = Task("学类")
second = Task("学 FastAPI")
first.tags.append("python")
print(first.category) # todo
print(second.category) # todo
print(first.tags) # ['python']
print(second.tags) # []属性查找可以先记成:
先找实例自己的属性;找不到,再沿着类和父类继续找。
最危险的坑:把可变对象写成类属性
class BadTask:
# 错误:这个列表由所有实例共享
tags: list[str] = []
first = BadTask()
second = BadTask()
first.tags.append("python")
print(second.tags) # ['python'],被另一个实例污染正确写法:
class Task:
def __init__(self) -> None:
# 每次实例化都会新建一个列表
self.tags: list[str] = []TypeScript
class Task {
// static 才是类本身共享的属性
static category = "todo";
// 实例字段会为每个实例初始化
tags: string[] = [];
}补充
- 声明:
- 类直接赋值即为
类属性,无需static关键字- 同 TS 的
static字段
- 同 TS 的
- 类直接赋值即为
- 读写:
- 读:
- 实例可直接读取
类属性;
- 实例可直接读取
- 写:
- 但通过实例赋值会有
副作用。
- 但通过实例赋值会有
- 读:
实例方法、类方法与静态方法
Python 将类体内的函数划分为三类:
- 实例方法:
- 自动绑定实例
self
- 自动绑定实例
- 类方法(
@classmethod):- 自动绑定当前类
cls
- 自动绑定当前类
- 静态方法(
@staticmethod):- 什么都不绑定,还原为普通函数
理解关键在于:底层的函数绑定机制,以及如何准确映射到 TS/JS 的
static机制。
语法底层:装饰器做了什么?
- 无装饰器(默认):
- 通过类调用(
User.method):- 属于普通函数,不绑定任何参数。
- 通过实例调用(
user.method):- 属于绑定方法,自动把
user实例作为第一个参数传入self。
- 属于绑定方法,自动把
- 通过类调用(
@classmethod:- 无论通过
类还是实例调用,都绑定为以当前类对象为首个参数cls的方法。
- 无论通过
@staticmethod:- 无论通过
类还是实例调用,都还原为普通函数,不会自动注入self或cls。
- 无论通过
区别在于 是通过
类调用还是 通过实例调用
实例方法(Instance Method)
- 核心用途:
- 读写某个具体实例自身的数据与状态。
- 参数绑定:
- 首参固定为
self。
- 首参固定为
- TS 对照:
- 完全等价于 TS 类中的普通原型方法(通过
this访问实例)。
- 完全等价于 TS 类中的普通原型方法(通过
# Python
class User:
def __init__(self, name: str) -> None:
self.name = name
def update_name(self, new_name: str) -> None:
self.name = new_name// TS 对照
class User {
name: string;
constructor(name: string) {
this.name = name;
}
updateName(newName: string): void {
this.name = newName;
}
}类方法(@classmethod)
- 核心用途:凡是需要显式操作“当前类
cls”的场景。- 备用构造器 / 工厂模式(配合继承实现多态实例化)。
- 读取或修改类级别的共有属性。
- 参数绑定:
- 首参固定为
cls。
- 首参固定为
TS 对照与关键差异
TS 的 static 方法中,this 默认指向当前调用的构造函数。如果使用 new this(...),TS 同样具备多态构造能力。
两者的真实差异在于:
- TS 能力具备,但开发者极易习惯性硬编码
new User(...),从而丢失多态能力。 - Python 的
@classmethod在形参列表中直接显式声明cls,在语法签名层面强约束了“获取当前类”。
# Python:使用 @classmethod 实现备用构造器(多态实例化)
class User:
def __init__(self, name: str) -> None:
self.name = name
@classmethod
def from_json(cls, data: dict) -> "User":
# cls 代表调用该方法的类(可能是 User,也可能是其子类)
print(f"当前传入的 cls 是: {cls}")
# cls(name=...) 等价于调用当前类的 __init__ 创建新实例
instance = cls(name=data["name"])
# 必须显式 return!与 __init__ 禁止 return 不同,工厂函数必须将对象交付给外部调用者
return instance
class VipUser(User):
# 子类继承父类的 from_json 类方法
pass
# 通过父类调用:cls 动态绑定为 User 类
u1 = User.from_json({"name": "张三"})
# 输出: 当前传入的 cls 是: <class '__main__.User'>
print(f"u1 的真实类型: {type(u1)}")
# 输出: u1 的真实类型: <class '__main__.User'>
print(f"u1 是否为 User 实例: {isinstance(u1, User)}")
# 输出: u1 是否为 User 实例: True
print("-" * 30)
# 通过子类调用:cls 动态绑定为 VipUser 类
u2 = VipUser.from_json({"name": "李四"})
# 输出: 当前传入的 cls 是: <class '__main__.VipUser'>
print(f"u2 的真实类型: {type(u2)}")
# 输出: u2 的真实类型: <class '__main__.VipUser'>
print(f"u2 是否为 VipUser 实例: {isinstance(u2, VipUser)}")
# 输出: u2 是否为 VipUser 实例: True语法细节:为什么类型注解写成 "User" 字符串?
- 现象:在类体定义内部,
User类尚未完全创建完成。直接写-> User:会触发NameError: name 'User' is not defined。 - 解法:使用字符串
"User"作为前向引用(Forward Reference),供静态类型检查工具(如 IDE、mypy)识别。 - 终极方案:Python 3.7+ 可以在文件最开头加上
from __future__ import annotations,之后便可直接写-> User:而无需加引号。
// TS 对照:通过 new this(...) 实现多态实例化
class User {
name: string;
constructor(name: string) {
this.name = name;
}
// this 指向调用该静态方法的类构造函数
static fromJson(this: typeof User, data: { name: string }) {
console.log("当前 this 构造函数是:", this.name);
return new this(data.name);
}
}
class VipUser extends User {}
// 父类调用
const u1 = User.fromJson({ name: "张三" });
// 控制台输出: 当前 this 构造函数是: User
console.log("u1 的类型是否为 User:", u1 instanceof User);
// 控制台输出: u1 的类型是否为 User: true
// 子类调用
const u2 = VipUser.fromJson({ name: "李四" });
// 控制台输出: 当前 this 构造函数是: VipUser
console.log("u2 的类型是否为 VipUser:", u2 instanceof VipUser);
// 控制台输出: u2 的类型是否为 VipUser: true静态方法(@staticmethod)
- 核心用途:
- 将一个与类存在逻辑关联、但完全不碰实例(
self)也不碰类(cls)的纯工具函数收纳在类命名空间下。
- 将一个与类存在逻辑关联、但完全不碰实例(
- 参数绑定:无隐式参数。
TS 对照
TS 的 static 关键字同时承担了 Python 中 @classmethod 与 @staticmethod 的双重角色:
- 若 TS 的
static方法使用this(当作构造函数或访问静态属性),概念映射为 Python 的@classmethod。 - 若 TS 的
static方法完全不碰this(纯工具函数),概念映射为 Python 的@staticmethod。
# Python:纯工具逻辑
class Validator:
@staticmethod
def is_valid_email(email: str) -> bool:
return "@" in email and "." in email// TS 对照:纯工具逻辑(不使用 this)
class Validator {
static isValidEmail(email: string): boolean {
return email.includes("@") && email.includes(".");
}
}- 选型建议:
- 在 Python 中,如果函数既不用
self也不用cls,且无需在类命名空间中隔离,优先直接定义为模块顶级函数,比使用@staticmethod更符合 Python 规范。
- 在 Python 中,如果函数既不用
“私有”只是约定和名称改写
Python
class TaskService:
def __init__(self) -> None:
# 单下划线:约定为内部实现,外部仍然能访问
self._next_id = 1
# 双下划线:触发名称改写,不是真正不可访问
self.__token = "internal"
def create_id(self) -> int:
task_id = self._next_id
self._next_id += 1
return task_idTypeScript
class TaskService {
// private 主要由 TypeScript 类型系统限制访问
private nextId = 1;
// `#token` 是 JavaScript 运行时私有字段
`#token` = "internal";
}补充
_name:- 告诉调用者“这是内部实现”,解释器不阻止访问。
- 君子之约而已
__name:- 会改写成近似
_ClassName__name,主要用于避免子类意外撞名。
- 会改写成近似
__name仍可被外部绕过,不等于 JS 的#name。__init__、__repr__这种前后都有双下划线的是 Python 预留的特殊方法,和只在开头写双下划线不是一回事。
用 @property 与 @<prop>.setter 控制属性读写
解决的核心问题
- 消除无意义的 Getter/Setter 方法:
- 无需像 Java/C++ 那样事先写满
get_title()和set_title()。
- 无需像 Java/C++ 那样事先写满
- 保持外部接口一致:
- 外部始终以
task.title(读取)和task.title = "xxx"(赋值)的形式调用,内部平滑替换为方法逻辑。
- 外部始终以
- 保护底层字段:
- 通常配合下划线约定字段(如
self._title),防止外部绕过校验直接修改。
- 通常配合下划线约定字段(如
语法结构与核心组件
- Getter(读取器):
- 使用
@property装饰,定义读取逻辑。
- 使用
- Setter(写入器):
- 使用
@<prop_name>.setter装饰,定义赋值校验逻辑。必须与 Getter 方法同名。
- 使用
- Deleter(删除器):
- (可选)使用
@<prop_name>.deleter装饰,定义执行del task.title时的清理逻辑。
- (可选)使用
代码 Demo 与运行验证
# Python:使用 @property 和 @setter 管理属性访问
class Task:
def __init__(self, title: str) -> None:
# 内部真正的存取变量约定加上单下划线 _title
self._title = title.strip()
# Getter:读取 task.title 时隐式触发
@property
def title(self) -> str:
print("触发 Getter:读取 title")
return self._title
# Setter:给 task.title = "xxx" 赋值时隐式触发
@title.setter
def title(self, value: str) -> None:
print(f"触发 Setter:准备写入新值 '{value}'")
normalized = value.strip()
if not normalized:
raise ValueError("title 不能为空")
self._title = normalized
# Deleter:执行 del task.title 时隐式触发
@title.deleter
def title(self) -> None:
print("触发 Deleter:清理 title")
del self._title
task = Task(" 初始任务 ")
# 触发 Getter
print(f"当前标题: {task.title}")
# 输出: 触发 Getter:读取 title
# 输出: 当前标题: 初始任务
print("-" * 30)
# 触发 Setter 赋值与校验
task.title = " 更新后的任务 "
# 输出: 触发 Setter:准备写入新值 ' 更新后的任务 '
print(f"更新后标题: {task.title}")
# 输出: 触发 Getter:读取 title
# 输出: 更新后标题: 更新后的任务
print("-" * 30)
# 触发 Deleter
del task.title
# 输出: 触发 Deleter:清理 title// TS 对照:使用 get / set 关键字
class Task {
`#title:` string;
constructor(title: string) {
this.#title = title.trim();
}
// Getter
get title(): string {
console.log("触发 Getter");
return this.#title;
}
// Setter
set title(value: string) {
console.log("触发 Setter");
const normalized = value.trim();
if (!normalized) {
throw new Error("title 不能为空");
}
this.#title = normalized;
}
}
const task = new Task(" 初始任务 ");
console.log("当前标题:", task.title);
task.title = " 更新后的任务 ";关键踩坑点:无限递归(RecursionError)
在 Getter 或 Setter 内部,切记不能直接操作与 @property 同名的属性:
class WrongTask:
@property
def title(self) -> str:
# 错误!内部又访问了 self.title,会再次触发 @property Getter,导致死循环抛出 RecursionError
return self.title正确做法是必须在内部维护一个下划线前缀的属性(如
self._title)。
使用建议
- 适用场景:
- 简单属性的读写校验、计算属性(如由
first_name和last_name拼接full_name)、只读属性(只写@property不写@setter)。
- 简单属性的读写校验、计算属性(如由
- 避免滥用:
- 耗时较长、涉及网络 I/O、数据库查询或带有复杂副作用的操作,应明确写成普通方法(如
fetch_title()),不要隐藏在属性读取之后。
- 耗时较长、涉及网络 I/O、数据库查询或带有复杂副作用的操作,应明确写成普通方法(如
继承、重写与 super()
Python
class Task:
def __init__(self, task_id: int, title: str) -> None:
self.task_id = task_id
self.title = title
def label(self) -> str:
return f"{self.task_id}: {self.title}"
class UrgentTask(Task):
def __init__(self, task_id: int, title: str, deadline: str) -> None:
# 让继承链中的下一个实现完成父类初始化
super().__init__(task_id=task_id, title=title)
self.deadline = deadline
# 重写父类方法
def label(self) -> str:
return f"[紧急] {super().label()},截止 {self.deadline}"TypeScript:extends
class UrgentTask extends Task {
constructor(
taskId: number,
title: string,
public deadline: string,
) {
super(taskId, title);
}
override label(): string {
return `[紧急] ${super.label()},截止 ${this.deadline}`;
}
}补充
- Python 继承写成
class UrgentTask(Task):,不用extends。 - Python 没有 TS 的
override关键字;是否正确重写主要靠类型检查器、测试和代码审查。 - Python 支持多继承:
class C(A, B):方法到底找谁由C.mro()给出的 MRO 决定。
什么是 MRO 与 super() 的真实含义?
MRO 是 Method Resolution Order(方法解析顺序)的缩写。
当一个类继承了多个父类时,Python 需要一套明确的规则来决定成员和方法的查找路线。
- 查找顺序:
C.mro()会返回一个有序的类列表(例如[C, A, B, object]),Python 在寻找属性或方法时,会严格自左向右按该列表顺序查找,谁先拥有就优先使用谁的。 - super() 的真实含义:
super()并不是直接调用父类,而是调用 MRO 列表中的下一个类。在多继承(如菱形继承)中,这会导致super()调用到一个看起来跟当前类没有直接继承关系的兄弟类。
- 普通业务代码优先使用“组合”代替多继承,继承关系保持浅显;
- 多继承和 MRO 复杂的链条一旦出现,维护成本会急剧增加。
dataclass、Pydantic 模型与普通类不是一回事
在 Python 中,处理“数据”有三种主要方式:普通类、dataclass 和 Pydantic BaseModel。
它们并非相互替代,而是为了解决不同阶段、不同场景下的具体痛点。
四种“数据与结构定义方式”核心定位速查
- 普通类(Plain Class)
- 背景定位:Python 原生的面向对象基石
- 适用场景:包含复杂业务逻辑方法、带有内部状态变更的对象
- 缺点:用于纯数据传递(DTO)时,手写初始化与打印逻辑导致样板代码冗长
- 数据类(
dataclass)- 背景定位:Python 3.7 引入的标准库工具
- 适用场景:内存中的纯数据容器、
DTO 对象(只存数据,少写样板代码) - 缺点:只负责自动生成构造与打印代码,默认不做运行时类型校验与转换
- Pydantic 模型(
BaseModel)- 背景定位:目前最流行的
第三方数据校验框架(FastAPI 的数据基石) - 适用场景:Web API 接口输入输出、配置文件读取、JSON 数据解析等外部数据边界
- 核心优势:强大的运行时数据校验、类型解析与自动转换
- 背景定位:目前最流行的
- 结构化协议(
Protocol)- 背景定位:Python 3.8 引入的鸭子类型协议(结构类型系统)
- 适用场景:不需要显式继承,只要方法签名对得上就能被类型检查器认可
- TS 对照:最接近 TS 中的
interface
普通类(Plain Class):手写样板代码的痛苦
在没有 dataclass 前,定义一个简单的纯数据对象,需要手写大量的 __init__、__repr__(打印信息)、__eq__(相等比较):
class PlainTask:
def __init__(self, task_id: int, title: str, done: bool = False) -> None:
self.task_id = task_id
self.title = title
self.done = done
t1 = PlainTask(1, "写代码")
print(t1)
# 输出: <__main__.PlainTask object at 0x1023a4510> (看不起具体属性内容!)
t2 = PlainTask(1, "写代码")
print(t1 == t2)
# 输出: False (即使属性全一样,由于默认比较内存地址,仍为 False!)@dataclass:标准库的“免样板代码”利器
为了解决普通类装载数据时的笨重,Python 3.7 引入了标准库 @dataclass 装饰器。
核心作用
自动为你生成 __init__、友好可读的 __repr__ 打印、以及基于属性值比较的 __eq__。
from dataclasses import dataclass, field
@dataclass(slots=True)
class DataTask:
task_id: int
title: str
done: bool = False
# 可变默认值(如 list/dict)必须使用 default_factory,防止所有实例共享同一个列表
tags: list[str] = field(default_factory=list)
t1 = DataTask(1, "写代码")
print(t1)
# 输出: DataTask(task_id=1, title='写代码', done=False, tags=[]) (自动生成清晰的打印!)
t2 = DataTask(1, "写代码")
print(t1 == t2)
# 输出: True (自动按属性值比较,不再死板比较内存地址!)什么是 slots=True?
- 默认的 Python 类:每个实例对象底层都有一个叫
__dict__的字典,用来保存它的所有属性。因为字典支持动态添加任意新属性,所以很灵活,但代价是内存开销很大(每个对象需要额外消耗约 100~150 字节的字典结构开销)。 - slots=True 的作用:告诉 Python 放弃动态字典,只在内存中留出固定几个字段(
task_id、title等)的存储空间。 - 带来的两个好处:
- 极大节省内存:如果程序在内存里需要一次性加载几十万个数据对象,内存占用能降低 30% 到 50%。
- 防止意外写错属性:因为固定了字段,外部如果尝试给实例添加未定义的属性(比如
t1.titlee = "..."拼写错误),Python 会直接抛出 AttributeError,防止悄悄创建了一个垃圾属性。
关键边界:它不等于“类型校验”
许多人容易误以为加了类型注解 task_id: int,dataclass 就会校验类型。
实际上,dataclass 运行时默认完全不拦截非法类型:
# 传入字符串 "不是整数",运行时默认完全不报错!
bad_task = DataTask(task_id="不是整数", title="测试")
print(bad_task)
# 输出: DataTask(task_id='不是整数', title='测试', done=False, tags=[])Pydantic BaseModel:FastAPI 的运行时数据边界
当进入 Web 开发(如 FastAPI),前端传来的 JSON 永远是外部不安全的数据。此时只用 @dataclass 是不够的,你需要 Pydantic。
核心定义与作用
Pydantic 是一个第三方库,通过继承 pydantic.BaseModel,提供运行时强力数据校验与类型自动转换(Parsing)。
代码 Demo 与校验验证
from pydantic import BaseModel, Field
class TaskSchema(BaseModel):
task_id: int
title: str = Field(min_length=1, max_length=50) # 约束字符串长度
done: bool = False
# 1. 自动类型转换(把字符串 "100" 自动转为整数 100)
t1 = TaskSchema(task_id="100", title="学习 FastAPI")
print(t1)
# 输出: task_id=100 title='学习 FastAPI' done=False
print(f"task_id 的实际类型: {type(t1.task_id)}")
# 输出: task_id 的实际类型: <class 'int'>
# 2. 运行时类型校验拦截(传入不合格数据直接抛出 ValidationError 异常)
try:
TaskSchema(task_id="abc", title="")
except Exception as e:
print("触发校验失败报错:")
print(e)
# 输出: 报错信息,清晰指明 task_id 需要合法整数,title 长度不能小于 1Protocol:最接近 TS interface 的鸭子类型协议
在 TS 中,interface 是纯粹的“结构类型系统”(只要形状一样就能赋值)。
Python 3.8 引入了 Protocol 来提供相同的能力。
对齐 TS 认知:名义类型 vs 结构类型
- 名义类型(Nominal Typing):在 Java、C++ 或传统 Python 抽象类中,如果一个函数声明了需要
Renderable类型的参数,传入的对象所属的类必须显式继承自Renderable(例如class Button(Renderable)),否则类型检查器会报错。这是基于“名字和继承关系”的校验。 - 结构类型(Structural Typing):在 TypeScript 中,接口是结构化的。如果函数要求参数满足
interface Renderable,你只需传入一个写了render(): string方法的任何对象即可,无需显式继承。这是基于“形状和行为”的校验,也被称为“鸭子类型(如果它走起路来像鸭子,叫起来也像鸭子,那它就是鸭子)”。
Python 的 Protocol 就是为了让静态类型检查器(如 mypy、Pyright)也支持这种“看形状、不看继承”的结构类型校验。
核心定义
Protocol 允许你定义一个接口规范,实现类无需显式继承该 Protocol,只要实现了相同的方法签名,静态检查器就会认可。
from typing import Protocol
# 定义接口协议(无需被直接继承)
class Renderable(Protocol):
def render(self) -> str:
...
class Button:
def render(self) -> str:
return "<button>按钮</button>"
def display(item: Renderable) -> None:
print(item.render())
# Button 没有继承 Renderable,但结构匹配,可通过类型检查!
display(Button())
# 输出: <button>按钮</button>数据结构选型总结
- 普通类:
- 写复杂业务逻辑、有状态交互时使用(最传统)。
dataclass:- 只在内存中传递数据,追求轻量,无需校验外部输入时使用(标准库内置)。
- Pydantic (
BaseModel):- 处理 API 请求/响应、配置文件、需要严格校验和类型转换时使用(FastAPI 标配,TS 中对应 Zod / Valibot)。
Protocol:- 做静态类型抽象、不需要显式继承的结构约束时使用(TS 中对应
interface)。
- 做静态类型抽象、不需要显式继承的结构约束时使用(TS 中对应
ORM 类:SQLAlchemy 与 SQLModel 的区别
在 Web 开发中,除了上述几种数据定义方式,最常接触到的就是 ORM 类(对象关系映射)。
什么是 ORM 类?
用来把数据库里的表结构和单行记录,映射成 Python 的类和对象。这样你就可以直接用 Python 代码读写数据库,不用手写 SQL 语句。
在 FastAPI 生态中,最常用的两个 ORM 库是 SQLAlchemy 和 SQLModel:
- 它们的关系
- SQLModel 是在
SQLAlchemy和 Pydantic 之上包装出来的。它的底层就是SQLAlchemy负责数据库,Pydantic负责校验。
- SQLModel 是在
- 它们解决的痛点
- 原生 SQLAlchemy 做法:
- 你需要先定义一个 Pydantic 类用来做 API 接口的数据校验,再定义一个 SQLAlchemy 类做数据库映射。这两个类里的字段高度重复,修改字段需要改两份代码。
- 原生 SQLAlchemy 做法:
- SQLModel 做法:
- 把这两者合二为一。你定义一个 SQLModel 类,它既是 Pydantic 模型,也是 ORM 模型,只需要定义一次,两处复用。
- 它们的使用决策
- 常规的 FastAPI 项目,优先选择
SQLModel,可以大幅减少重复的样板代码,体验更顺畅。
- 常规的 FastAPI 项目,优先选择
- 非常庞大、关联关系极度复杂、需要深度定制数据库特性的项目,直接使用原生的 SQLAlchemy 2.0 配合原生 Pydantic 自由度更高,控制边界更清晰。
双下划线方法(Dunder Methods):让对象接入 Python 语法
概念科普与名词解释
- Dunder 的由来:
- Double Underscore(双下划线)的简称。指的是前后各有两个下划线的方法(如
__init__)。
- Double Underscore(双下划线)的简称。指的是前后各有两个下划线的方法(如
- 核心作用:
- Python 的语法糖基石(操作符重载)。
- 解释器遇到内置语法(如
len(obj)、print(obj)、for in循环)时,会在底层隐式调用对应的 Dunder 方法。
- 常用魔法方法速查:
__init__:- 构造初始化方法(创建对象时自动调用)。
__str__:- 字符串转化方法。
str(obj)或print(obj)时给用户看的易读输出。
- 字符串转化方法。
__repr__:- 调试输出方法。在终端直接回车或查看对象列表时显示的开发调试信息。
- 即 print 输出
__len__:- 长度获取方法。调用
len(obj)时触发。
- 长度获取方法。调用
__iter__:- 迭代器协议方法。让对象支持
for item in obj循环遍历。
- 迭代器协议方法。让对象支持
__eq__:- 相等比较方法。当执行
obj1 == obj2时触发。
- 相等比较方法。当执行
__call__:- 可调用对象方法。让类实例可以像普通函数一样使用
obj()被直接调用。
- 可调用对象方法。让类实例可以像普通函数一样使用
代码 Demo 与内置语法接轨
# Python:使用 Dunder 方法让自定义类接入 Python 内置语法
class TaskCollection:
def __init__(self, tasks: list[str]) -> None:
self._tasks = tasks
# 接入 len() 语法
def __len__(self) -> int:
return len(self._tasks)
# 接入 for ... in 循环遍历语法
def __iter__(self):
return iter(self._tasks)
# 接入 print() 与调试输出
def __repr__(self) -> str:
return f"TaskCollection(total={len(self)})"
collection = TaskCollection(["任务一", "任务二"])
# 1. 触发 __len__
print(f"任务数量: {len(collection)}")
# 输出: 任务数量: 2
# 2. 触发 __repr__
print(f"集合对象: {collection}")
# 输出: 集合对象: TaskCollection(total=2)
# 3. 触发 __iter__
for task in collection:
print(f"当前遍历: {task}")
# 输出: 当前遍历: 任务一
# 输出: 当前遍历: 任务二什么时候用类,什么时候只用函数和模块
选型决策:什么时候用类,什么时候用函数?
- 选择使用
类:- 同一组数据和行为必须绑定在一起
- 一个对象要维护明确的生命周期或状态
- 需要通过
依赖注入替换实现 - 需要利用某种协议、继承关系或 Python 数据模型
- 优先使用
函数和模块:- 逻辑只依赖参数,返回结果,不保存状态
- 只是把几个工具函数分组
- 写成类后几乎全是
@staticmethod - 唯一的理由是“传统面向对象惯性”
FastAPI 后端分工边界速查
- 路由函数:连接 HTTP 请求与业务调用
- Pydantic 模型:校验外部输入,约束响应输出
- 普通函数:完成
无状态转换逻辑 - 普通类:封装
有状态协作、资源或一组业务行为 Depends:声明依赖提供者(按请求创建或复用,不等于全局单例)- ORM 类:映射数据库表与单行记录
Session:数据库工作单元,通过依赖按请求管理
补充:针对 JS/TS 开发者的 Python 类机制避坑指南
这里整理了几个直接影响真实编码、类型检查和 FastAPI 开发的底层机制。这些地方如果用 JS/TS 的思维来套,非常容易写出 Bug。
避坑一:类里面的代码在导入时就会执行(类本身也是个运行时对象)
- 与 JS/TS 对比:
- 在 JS/TS 中,
class是一份静态声明。但在 Python 中,类是一个真正的运行时对象。
- 在 JS/TS 中,
- 执行时机:
- 当 Python 执行到
class Task:- 这行代码时,会立即从上到下执行类体内部的所有代码,然后再创建出这个类。
- 当 Python 执行到
- 典型表现:
- 类属性的默认值计算、直接在类体里执行的表达式,都会在
导入(import)该模块时被触发,而不是在实例化时触发。
- 类属性的默认值计算、直接在类体里执行的表达式,都会在
def load_default_category() -> str:
print("只要导入这个文件,这行字就会立即打印!")
return "todo"
class Task:
# 模块首次加载、还没实例化时,这行代码就会被执行并调用函数!
category = load_default_category()
def __init__(self, title: str) -> None:
self.title = title- 核心警示:
- 千万不要在类体内部直接写数据库连接、网络请求等有
副作用的代码(除了定义方法和常量)。 - 任何地方只要导入这个文件,这些代码就会立刻同步运行,导致测试、启动或部署时发生各种难以预料的问题。
- 千万不要在类体内部直接写数据库连接、网络请求等有
避坑二:类型注解只是摆设,不代表会自动进行运行时校验
- 与 TS 对比:
- TypeScript 编译后擦除类型;
- Python 的类型注解虽然保留在运行时,但默认情况下解释器完全忽略它。
- 运行时表现:
- 即使声明了
title: str,依然可以直接传入整数等任意非法类型,Python 运行时默认根本不会报错。
- 即使声明了
class Task:
# 这里的 title: str 只是给 IDE 看的提示,Python 运行时根本不阻止你传入别的数据类型
title: str
def __init__(self, title: str) -> None:
self.title = title
task = Task("任务一")
# Python 运行时完全允许你传入一个整数,不会报错!
task.title = 123- 防线划分:
- IDE 与静态检查:
- 类型注解主要服务于 IDE 和静态类型检查工具(如 mypy、Pyright),用于开发期提示。
- 标准库 dataclasses:
- 仅用于自动生成
__init__等模板代码,依然不会在运行时校验类型。
- 仅用于自动生成
- Pydantic BaseModel:
- 只有继承自 Pydantic 的类,才会在运行时主动读取类型注解并执行强数据校验与类型自动转换。
- IDE 与静态检查:
避坑三:对象属性是完全敞开的,默认可以随便在外部加新属性
- 与 JS/TS 对比:
- JS/TS 类的属性是比较严格的,不能在类定义之外随便加新属性。
- 默认表现:
- Python 的普通对象默认是极其松散的,底层使用
__dict__字典来存储属性,支持在外部直接动态添加任意新属性。
- Python 的普通对象默认是极其松散的,底层使用
class LooseTask:
def __init__(self, title: str) -> None:
self.title = title
loose = LooseTask("普通任务")
# 运行时直接塞入一个完全没声明过的 owner 属性,完全合法!
loose.owner = "liguwe"
print(vars(loose)) # 打印出所有属性:{'title': '普通任务', 'owner': 'liguwe'}- slots 的作用:
- 内存减半:
- 省去了动态字典的结构开销,对大批量对象非常划算。
- 拼写拦截:
- 一旦在外部访问或修改未定义字段(例如拼写错成
loose.ownerr = "..."),直接抛出 AttributeError 报错拦截。可以通过@dataclass(slots=True)快速开启。
- 一旦在外部访问或修改未定义字段(例如拼写错成
- 内存减半:
避坑四:is 比较的是内存地址,== 比较的是值
- is 检查:
- 判断两个变量是否指向内存中的同一个对象(类似 JS 中的
===但比它更关注地址)。 - 最常用于跟单例判断(比如
value is None)。
- 判断两个变量是否指向内存中的同一个对象(类似 JS 中的
==检查:- 判断两者的内容或字段值是否相等。它在底层会调用
__eq__方法。
- 判断两者的内容或字段值是否相等。它在底层会调用
class PlainTask:
def __init__(self, task_id: int) -> None:
self.task_id = task_id
first = PlainTask(1)
second = PlainTask(1)
print(first is second) # False:因为是两个独立的内存对象
print(first == second) # False:普通类默认也是按内存地址比较,值相同也没用- 默认逻辑差异:
- 普通类:
- 如果没有定义
__eq__,==默认退化为is(即使两个不同实例的属性值完全相同,==也会返回 False)。
- 如果没有定义
- 数据模型类:
@dataclass或Pydantic会自动生成按字段内容比较的__eq__,所以==可以直接比较字段值。
- 普通类:
避坑五:Python 3.12 的三个高频类型新语法
- Self 类型:
- 应用场景:用于方法返回
self(链式调用)或类方法作为工厂函数(classmethod)创建子类实例时。 - 作用:类型检查器会自动将其推导为实际调用的子类类型,而不是固定死在父类。
- 应用场景:用于方法返回
- @override 装饰器:
- 作用:类似 TS 的
override关键字,表示该子类方法是覆盖父类的。
- 作用:类似 TS 的
- 校验机制:
- 仅在静态类型检查(如 Pyright)阶段生效,防止重构时写错父类方法名;
- 运行时没有任何校验或执行开销。
- 泛型类新语法
class Box[T]:- 作用:对应 TS 的
class Box<T>,定义泛型类时不再需要写繁琐的TypeVar声明。
- 作用:对应 TS 的
from typing import Self, override
class Task:
def __init__(self, title: str) -> None:
self.title = title
def rename(self, title: str) -> Self:
self.title = title
return self
class UrgentTask(Task):
@override
def rename(self, title: str) -> Self:
return super().rename(f"[紧急] {title}")避坑六:Protocol 与抽象基类(ABC)解决不同的问题
- Protocol(结构类型系统):
- 特征:关注“看起来像就行”(鸭子类型)。不需要显式继承,只要类里实现了相同名字和签名的方法,就能通过类型校验。
- TS 对比:非常类似 TS 的
interface。- 适用场景:最适合用于依赖注入、测试替身和第三方解耦。
- ABC(抽象基类 / 名义类型系统):
- 特征:关注“继承关系是否正统”。子类必须显式继承自抽象父类,并实现所有被
@abstractmethod标记的方法。
- 特征:关注“继承关系是否正统”。子类必须显式继承自抽象父类,并实现所有被
- 运行时拦截:
- 若子类未实现抽象方法,Python 运行时会在实例化时直接抛错拦截。
- 适用场景:
- 适合在团队内部强制规范复杂的类层级关系与架构约束。
避坑七:FastAPI 的 Depends(Class) 每次请求都会创建新对象,不是全局单例
- 与 Java/Spring 对比:
- Spring 或 NestJS 的依赖注入通常默认是
全局单例容器。
- Spring 或 NestJS 的依赖注入通常默认是
- FastAPI 运行机制:
- 按请求构建:
- 每次收到新的 HTTP 请求,FastAPI 都会重新解析
Depends,读取类构造函数的入参,从请求参数中完成校验和类型转换,然后实例化一个崭新的对象注入路由函数。
- 每次收到新的 HTTP 请求,FastAPI 都会重新解析
- 生命周期闭环:
- 像数据库
Session等资源,必须使用带yield的依赖函数管理生命周期,不要直接作为 Service 类的属性长期共享。
- 像数据库
- 单请求缓存:
- 在同一个请求生命周期内,如果多个下级依赖共享同一个
Depends(Class),FastAPI 默认会缓存该实例;但不同请求之间完全独立。
- 在同一个请求生命周期内,如果多个下级依赖共享同一个
- 按请求构建:
class Pagination:
def __init__(self, skip: int = 0, limit: int = 100) -> None:
self.skip = skip
self.limit = limit
# 声明依赖
PaginationDep = Annotated[Pagination, Depends()]
@app.get("/tasks")
def list_tasks(pagination: PaginationDep):
...到这里,类这一篇真正要形成的判断
- Python 类是运行时创建的对象,远比 TS 类型声明动态。
- 普通类型注解主要帮助静态检查;
- Pydantic 才负责 FastAPI 外部边界的运行时校验。
- 优先用实例方法表达对象行为,用类方法表达多态构造,用模块函数替代没有类状态的静态工具方法。
- 优先组合和
Protocol;- 只有确实存在稳定的“是一种”关系和共享机制时才使用继承或 ABC。
- FastAPI
Depends负责构建一次请求需要的对象图,不负责替业务代码决定分层,也不意味着全局单例。 - 类解决的是状态、行为和生命周期的组织问题;如果一个对象只有数据,用 dataclass 或 Pydantic;如果逻辑无状态,用函数。
最后,一个 HTTP 请求串起来所有
从 API 接收到数据库持久化的全流程
完整的请求链路、PostgreSQL 持久化和 FastAPI 后端生态,单独整理在:【内部:一个 HTTP 请求怎样串起 FastAPI 后端开发全貌?】。
参考
- Python 3.12 官方教程:Classes
- Python 3.12 官方文档:Data model
- Python 3.12 官方文档:dataclasses
- Python 3.12 官方文档:typing.Protocol
- Python 3.12 官方文档:typing.Self
- Python 3.12 官方文档:typing.override
- Python 3.12 官方文档:Generic classes
- Python 3.12 官方文档:abc
- FastAPI 官方文档:Request Body
- FastAPI 官方文档:Classes as Dependencies
- FastAPI 官方文档:Sub-dependencies
- FastAPI 官方文档:SQL (Relational) Databases
- SQLModel 官方文档:Session with FastAPI Dependency
- SQLAlchemy 2.0 官方文档:ORM Quick Start
- SQLAlchemy 2.0 官方文档:Session Basics
- Alembic 官方文档:Tutorial
- Pydantic 官方文档:Models
- 旧笔记:Python 类
- 同系列:Python 篇一、Python 篇二、Python 篇三