Python面向对象
面向对象用于把数据和行为封装在一起。Python 中的类可以表示业务对象、服务对象、配置对象,也可以用于组织复杂逻辑。
零基础可以先这样理解:类是对象的模板,对象是模板创建出来的具体实例。
为什么需要面向对象
简单脚本可以只写函数,但项目变大后,数据和行为会越来越多。如果用户、订单、任务、配置都用零散 dict 传来传去,字段含义和修改逻辑会散落各处。
面向对象的价值是把相关数据和行为收拢到一个概念里。例如 Order 不只是一个字典,它可以有金额、状态,也可以有取消、支付、校验等行为。这样代码更容易表达业务模型。
从零基础角度看,面向对象主要解决三个问题:
| 问题 | 不用对象时 | 使用对象后 |
|---|---|---|
| 数据含义不清 | 到处传 dict,不知道有哪些 key | 类字段集中表达结构 |
| 规则散落 | 校验、状态流转写在多个函数里 | 行为收拢到对象或服务 |
| 依赖混乱 | 函数直接读全局变量或直接 new 依赖 | 通过构造函数显式传入依赖 |
面向对象不是“所有代码都必须写成类”。如果只是一个简单转换函数,用函数更清楚;当数据和行为长期一起变化时,类才更合适。
类和对象
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def say_hello(self):
print(f"hello {self.name}")
user = User("Tom", 18)
user.say_hello()关键点:
| 语法 | 含义 |
|---|---|
class User | 定义一个类 |
__init__ | 初始化对象 |
self | 当前对象本身 |
self.name | 对象属性 |
say_hello | 对象方法 |
对象创建流程
flowchart TD
A["调用类名 User(...)"] --> B["创建对象实例"]
B --> C["执行 __init__"]
C --> D["给 self 绑定属性"]
D --> E["返回对象"]
E --> F["调用对象方法"]更细一点理解:
User("Tom", 18)调用类对象。- Python 先创建一个空实例。
- Python 把这个实例作为
self传给__init__。 __init__给实例绑定属性。- 后续
user.say_hello()调用方法时,Python 也会自动把user作为self传入。
所以:
user.say_hello()可以粗略理解成:
User.say_hello(user)这就是为什么实例方法第一个参数通常写 self。
对象、引用和属性
Python 变量保存的不是对象本身,而是对象引用。
user1 = User("Tom", 18)
user2 = user1
user2.age = 20
print(user1.age) # 20user1 和 user2 指向同一个对象。
flowchart TD
A["变量 user1"] --> C["User对象<br/>name=Tom age=20"]
B["变量 user2"] --> C这和 list、dict 的可变对象问题是一致的。对象属性被修改后,所有引用这个对象的地方都会看到变化。
商业项目中要注意:
| 场景 | 风险 | 建议 |
|---|---|---|
| 多处共享同一个对象 | 一个地方修改影响其他地方 | 明确对象是否可变 |
| 把 ORM 实体到处传 | 事务关闭后访问懒加载字段报错 | Service 内转换为 DTO |
| 对象属性随便改 | 状态流转不可控 | 用方法表达状态变化 |
实例属性和类属性
class User:
category = "normal"
def __init__(self, name: str):
self.name = name| 类型 | 归属 | 示例 |
|---|---|---|
| 实例属性 | 每个对象独有 | self.name |
| 类属性 | 类共享 | User.category |
如果属性每个对象都不一样,放实例属性;如果所有对象共享,才考虑类属性。
类属性最容易出错的是放可变对象:
class ImportTask:
errors: list[str] = []
def add_error(self, message: str) -> None:
self.errors.append(message)多个实例会共享同一个 errors。
task1 = ImportTask()
task2 = ImportTask()
task1.add_error("A001 失败")
print(task2.errors) # ['A001 失败']正确写法:
class ImportTask:
def __init__(self) -> None:
self.errors: list[str] = []判断原则:
| 数据 | 应该放哪里 |
|---|---|
| 每个用户姓名 | 实例属性 |
| 每个任务错误列表 | 实例属性 |
| 所有对象共享的类型名称 | 类属性 |
| 常量配置 | 类属性或模块常量 |
方法类型
class User:
role = "member"
def __init__(self, name: str):
self.name = name
def instance_method(self):
return self.name
@classmethod
def class_method(cls):
return cls.role
@staticmethod
def static_method(value: str):
return value.strip()| 方法 | 第一个参数 | 适合场景 |
|---|---|---|
| 实例方法 | self | 需要访问对象属性 |
| 类方法 | cls | 需要访问类属性或创建类对象 |
| 静态方法 | 无固定参数 | 与类相关但不依赖对象状态 |
dataclass
如果类主要用于保存数据,优先考虑 dataclass。
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int使用:
user = User(name="Tom", age=18)
print(user)dataclass 会自动生成 __init__、__repr__ 等方法,适合 DTO、配置对象、简单数据模型。
dataclass 适合什么
dataclass 适合字段稳定、行为不复杂的数据对象。例如资产导入结果、分页参数、统计结果。
from dataclasses import dataclass
@dataclass
class AssetImportResult:
success_count: int
failed_count: int
error_file: str | None = None不可变数据可以加 frozen=True:
@dataclass(frozen=True)
class AssetQuery:
hospital_id: int
status: str
page_size: int = 20frozen=True 的意义是:查询条件创建后不应该被业务过程偷偷修改。这样更利于调试和测试。
dataclass 和 dict 怎么选
| 场景 | 建议 |
|---|---|
| 临时处理 JSON | dict |
| 字段稳定的内部数据结构 | dataclass |
| Web 入参出参校验 | Pydantic Model |
| 有复杂行为和状态流转 | 普通类 |
如果一个 dict 在项目里到处传,并且大家都要记住它有哪些 key,就应该考虑换成 dataclass 或 Pydantic。
继承
继承表示“子类是一种父类”。
class Animal:
def speak(self):
print("sound")
class Dog(Animal):
def speak(self):
print("wang")调用:
dog = Dog()
dog.speak()继承可以复用代码,但不要滥用。继承层级太深会让代码难理解。
继承适合表达稳定的 is-a 关系:
Dog 是一种 Animal
CsvAssetReader 是一种 AssetReader不适合表达“使用关系”:
OrderService 使用 EmailSenderOrderService 不应该继承 EmailSender,因为订单服务不是一种邮件发送器。
多态:同一个接口,不同实现
多态是面向对象里最有价值的能力之一。它让调用方只依赖统一接口,不关心具体实现。
class AssetReader:
def read(self, source: str) -> list[dict[str, str]]:
raise NotImplementedError
class CsvAssetReader(AssetReader):
def read(self, source: str) -> list[dict[str, str]]:
return [{"source": source, "type": "csv"}]
class ApiAssetReader(AssetReader):
def read(self, source: str) -> list[dict[str, str]]:
return [{"source": source, "type": "api"}]调用方:
def import_assets(reader: AssetReader, source: str) -> int:
rows = reader.read(source)
return len(rows)flowchart TD
A["import_assets"] --> B["AssetReader 接口"]
B --> C["CsvAssetReader"]
B --> D["ApiAssetReader"]
C --> E["读取 CSV"]
D --> F["调用接口"]调用方只知道 reader.read(),不关心数据来自 CSV 还是接口。新增 Excel 读取器时,新增一个类即可。
抽象类和协议
Python 可以用 abc 定义抽象类,明确子类必须实现什么方法。
from abc import ABC, abstractmethod
class AssetReader(ABC):
@abstractmethod
def read(self, source: str) -> list[dict[str, str]]:
pass如果子类没有实现 read,就不能实例化。
Python 也常用“鸭子类型”:只要对象有需要的方法,就可以用。类型标注里可以用 Protocol 表达这种结构化接口。
from typing import Protocol
class Reader(Protocol):
def read(self, source: str) -> list[dict[str, str]]:
...
def import_assets(reader: Reader, source: str) -> int:
return len(reader.read(source))Protocol 更灵活,适合“我不关心你继承谁,只关心你有没有这些方法”的场景。
| 方式 | 适合 |
|---|---|
| 抽象类 ABC | 框架内明确父子关系,统一部分公共逻辑 |
| Protocol | 只关心能力结构,降低继承耦合 |
| 普通类 | 业务对象或服务对象 |
组合优先
很多场景用组合比继承更清楚。
class EmailSender:
def send(self, email: str, content: str):
print(f"send to {email}: {content}")
class UserService:
def __init__(self, email_sender: EmailSender):
self.email_sender = email_sender
def notify(self, email: str):
self.email_sender.send(email, "welcome")UserService 不需要继承 EmailSender,只需要使用它。
组合的核心是“我使用你”,不是“我是一种你”。
商业项目里,Service 通常通过组合使用 Repository、Client、Sender:
class AssetRepository:
def save(self, asset: dict[str, str]) -> None:
print(f"save asset {asset['asset_code']}")
class AuditLogger:
def record(self, action: str, target: str) -> None:
print(f"audit {action} {target}")
class AssetImportService:
def __init__(self, repository: AssetRepository, audit_logger: AuditLogger):
self.repository = repository
self.audit_logger = audit_logger
def import_one(self, asset: dict[str, str]) -> None:
self.repository.save(asset)
self.audit_logger.record("IMPORT_ASSET", asset["asset_code"])这样做的好处:
AssetImportService的依赖一眼可见。- 测试时可以传入假的 Repository。
- 审计逻辑和存储逻辑可以独立替换。
- 不需要复杂继承层级。
测试时替换依赖:
class FakeRepository:
def __init__(self) -> None:
self.saved: list[dict[str, str]] = []
def save(self, asset: dict[str, str]) -> None:
self.saved.append(asset)这就是依赖注入的基本思想:对象需要什么依赖,就从构造函数传进来,而不是在内部偷偷创建。
魔术方法
魔术方法是 Python 预定义的特殊方法。
| 方法 | 作用 |
|---|---|
__init__ | 初始化对象 |
__str__ | 面向用户的字符串表示 |
__repr__ | 面向开发者的字符串表示 |
__len__ | 支持 len() |
__enter__ / __exit__ | 支持上下文管理器 |
示例:
class Cart:
def __init__(self):
self.items = []
def add(self, item: str):
self.items.append(item)
def __len__(self):
return len(self.items)
cart = Cart()
cart.add("book")
print(len(cart))上下文管理器
__enter__ 和 __exit__ 可以让对象支持 with,常用于资源管理。
class ManagedFile:
def __init__(self, path: str):
self.path = path
self.file = None
def __enter__(self):
self.file = open(self.path, "r", encoding="utf-8")
return self.file
def __exit__(self, exc_type, exc, traceback):
if self.file:
self.file.close()使用:
with ManagedFile("demo.txt") as file:
content = file.read()为什么重要:文件、数据库连接、锁、网络连接都需要释放资源。with 能保证即使中间抛异常,退出时也能执行清理逻辑。
私有属性约定
Python 没有严格的 private,但有命名约定:
| 写法 | 含义 |
|---|---|
name | 公开属性 |
_name | 内部使用,不建议外部直接访问 |
__name | 名称改写,主要避免子类冲突 |
不要依赖命名做安全控制。真正的权限控制要通过业务逻辑实现。
property:控制属性读写
如果一个属性不能被随便设置,可以用 property 包一层校验。
class Order:
def __init__(self, amount: float):
self.amount = amount
@property
def amount(self) -> float:
return self._amount
@amount.setter
def amount(self, value: float) -> None:
if value < 0:
raise ValueError("amount cannot be negative")
self._amount = value使用:
order = Order(100)
order.amount = -1 # ValueError这比到处手动校验更集中。不过也不要滥用 property,把复杂业务流程藏进属性赋值里会让代码难读。复杂状态变化更适合显式方法,例如 order.pay()、order.cancel()。
类设计流程
flowchart TD
A["识别业务对象"] --> B["列出对象属性"]
B --> C["列出对象行为"]
C --> D{"主要是保存数据吗"}
D -- "是" --> E["使用 dataclass"]
D -- "否" --> F["定义普通类"]
F --> G{"是否存在 is-a 关系"}
G -- "是" --> H["考虑继承"]
G -- "否" --> I["优先组合"]商业场景 Demo:资产导入领域模型
下面用一个完整但不复杂的例子,把 dataclass、普通类、组合、多态放在一起。
需求:
- 资产可以从 CSV 或接口读取。
- 读取后要校验资产编号和名称。
- 校验通过后保存。
- 保存后记录审计日志。
数据对象
from dataclasses import dataclass
@dataclass(frozen=True)
class Asset:
asset_code: str
asset_name: str
dept_code: str为什么 frozen=True:导入过程中的资产对象是一个清洗后的结果,不希望保存前被随便修改。如果要修改,应生成新对象。
读取接口
from typing import Protocol
class AssetReader(Protocol):
def read(self, source: str) -> list[Asset]:
...CSV 实现
class CsvAssetReader:
def read(self, source: str) -> list[Asset]:
return [
Asset(asset_code="A001", asset_name="CT", dept_code="D01"),
Asset(asset_code="A002", asset_name="MRI", dept_code="D02"),
]API 实现
class ApiAssetReader:
def read(self, source: str) -> list[Asset]:
return [
Asset(asset_code="B001", asset_name="DR", dept_code="D03"),
]Repository 和审计
class AssetRepository:
def save_all(self, assets: list[Asset]) -> None:
for asset in assets:
print(f"save {asset.asset_code}")
class AuditLogger:
def record(self, action: str, count: int) -> None:
print(f"audit action={action}, count={count}")Service 编排
class AssetImportService:
def __init__(
self,
reader: AssetReader,
repository: AssetRepository,
audit_logger: AuditLogger,
) -> None:
self.reader = reader
self.repository = repository
self.audit_logger = audit_logger
def import_assets(self, source: str) -> int:
assets = self.reader.read(source)
self._validate(assets)
self.repository.save_all(assets)
self.audit_logger.record("IMPORT_ASSET", len(assets))
return len(assets)
def _validate(self, assets: list[Asset]) -> None:
for asset in assets:
if not asset.asset_code:
raise ValueError("资产编号不能为空")
if not asset.asset_name:
raise ValueError("资产名称不能为空")使用
service = AssetImportService(
reader=CsvAssetReader(),
repository=AssetRepository(),
audit_logger=AuditLogger(),
)
count = service.import_assets("assets.csv")
print(count)这段代码的设计点:
| 设计 | 意义 |
|---|---|
Asset dataclass | 表达稳定数据结构 |
AssetReader Protocol | 调用方只依赖读取能力 |
CsvAssetReader / ApiAssetReader | 多态扩展不同来源 |
AssetImportService 组合依赖 | 不继承 reader/repository,而是使用它们 |
_validate 私有约定 | 校验逻辑收在服务内部 |
流程:
flowchart TD
A["AssetImportService"] --> B["AssetReader"]
B --> C["CsvAssetReader 或 ApiAssetReader"]
C --> D["返回 Asset 列表"]
D --> E["校验资产字段"]
E --> F["AssetRepository 保存"]
F --> G["AuditLogger 记录审计"]如果新增 Excel 导入,只需要新增 ExcelAssetReader,Service 不需要改。这就是多态和组合带来的扩展性。
面向对象和分层架构
Python Web 项目里常见分层:
api -> service -> repository/client -> database/external api对应对象:
| 层 | 常见对象 | 职责 |
|---|---|---|
| API | Router、Schema | 接收请求、参数校验、返回响应 |
| Service | AssetImportService | 业务编排、事务边界、规则判断 |
| Repository | AssetRepository | 数据库访问 |
| Client | HospitalApiClient | 外部接口访问 |
| DTO/Model | dataclass / Pydantic | 表达数据结构 |
不要让一个类横跨所有层。例如 AssetController 既读文件、又校验、又写库、又调外部接口,这会让代码很难维护。
生产排查:类设计问题怎么发现
flowchart TD
A["项目越来越难改"] --> B{"类是否职责过多"}
B -- "是" --> C["按 API/Service/Repository/Client 拆分"]
B -- "否" --> D{"继承层级是否太深"}
D -- "是" --> E["改用组合或 Protocol"]
D -- "否" --> F{"对象状态是否被乱改"}
F -- "是" --> G["收敛状态变化方法,使用 dataclass frozen"]
F -- "否" --> H["继续检查测试和依赖方向"]常见问题:
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 一个类几百行 | 职责过多 | 按领域和分层拆 |
| 子类改父类后到处坏 | 继承层级太深 | 优先组合,减少继承 |
| 测试需要真实数据库/接口 | 类内部偷偷创建依赖 | 构造函数注入依赖,测试用 fake |
| 对象状态莫名改变 | 可变对象共享或到处改属性 | 使用不可变数据对象或封装方法 |
| dict key 经常写错 | 缺少明确模型 | 用 dataclass/Pydantic |
常见问题
| 问题 | 原因 | 建议 |
|---|---|---|
| 类里什么都放 | 职责过多 | 按业务职责拆分 |
| 继承层级太深 | 过度抽象 | 优先组合 |
| 类属性存可变对象 | 多个实例共享状态 | 放到实例属性 |
| 方法依赖太多外部变量 | 难测试 | 通过构造函数传入依赖 |
| 数据类手写大量模板代码 | 重复劳动 | 使用 dataclass |
面试标准回答
Python 面向对象怎么理解
面向对象是把相关数据和行为封装到对象里,让代码更贴近业务概念。类是对象模板,对象是类创建出来的实例。实例属性保存每个对象自己的状态,方法表达对象行为。Python 中面向对象常用于领域模型、Service、Repository、Client、配置对象和 DTO。简单逻辑用函数即可,数据和行为关系复杂时再用类。继承和组合怎么选
继承适合稳定的 is-a 关系,比如 CsvReader 是一种 Reader;组合适合使用关系,比如 AssetImportService 使用 Repository 和 AuditLogger。商业项目里优先组合,因为组合依赖更清晰、测试更容易、继承层级更浅。继承层级太深会让父类修改影响很多子类。dataclass 适合什么
dataclass 适合字段稳定、行为不复杂的数据对象,例如查询条件、导入结果、DTO、配置快照。它能自动生成 __init__、__repr__ 等方法,减少模板代码。如果对象需要复杂状态流转和行为,应该用普通类;如果是 Web 入参出参校验,常用 Pydantic。Python 多态怎么实现
Python 多态可以通过继承、抽象类或 Protocol 实现。调用方只依赖统一方法,例如 reader.read(),具体对象可以是 CsvReader、ApiReader 或 ExcelReader。Python 也支持鸭子类型,只要对象有需要的方法就能使用。类型标注中可以用 Protocol 表达这种结构化接口。为什么要构造函数注入依赖
构造函数注入能让类依赖一眼可见,也方便测试时替换成 fake 或 mock。如果类内部自己创建数据库连接、HTTP 客户端或 Repository,测试会被真实外部依赖绑住,代码也更难扩展。组合加构造函数注入是 Python 项目工程化的重要习惯。练习
- 定义一个
User类,包含 name、age 和is_adult()方法。 - 用
dataclass定义一个Product数据对象。 - 写一个
Cart类,支持添加商品和计算数量。 - 用组合实现
OrderService调用EmailSender。 - 思考“狗继承动物”和“订单服务使用邮件服务”哪个适合继承,哪个适合组合。
小结
面向对象不是为了把所有代码都写成类,而是为了在数据和行为关系复杂时保持结构清晰。初学者重点掌握类、对象、self、实例属性、方法、dataclass、继承和组合。能用组合就不要强行继承,能用简单函数就不要硬造类。
