Skip to content

Python面向对象

面向对象用于把数据和行为封装在一起。Python 中的类可以表示业务对象、服务对象、配置对象,也可以用于组织复杂逻辑。

零基础可以先这样理解:类是对象的模板,对象是模板创建出来的具体实例。

为什么需要面向对象

简单脚本可以只写函数,但项目变大后,数据和行为会越来越多。如果用户、订单、任务、配置都用零散 dict 传来传去,字段含义和修改逻辑会散落各处。

面向对象的价值是把相关数据和行为收拢到一个概念里。例如 Order 不只是一个字典,它可以有金额、状态,也可以有取消、支付、校验等行为。这样代码更容易表达业务模型。

从零基础角度看,面向对象主要解决三个问题:

问题不用对象时使用对象后
数据含义不清到处传 dict,不知道有哪些 key类字段集中表达结构
规则散落校验、状态流转写在多个函数里行为收拢到对象或服务
依赖混乱函数直接读全局变量或直接 new 依赖通过构造函数显式传入依赖

面向对象不是“所有代码都必须写成类”。如果只是一个简单转换函数,用函数更清楚;当数据和行为长期一起变化时,类才更合适。

类和对象

python
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对象方法

对象创建流程

mermaid
flowchart TD
    A["调用类名 User(...)"] --> B["创建对象实例"]
    B --> C["执行 __init__"]
    C --> D["给 self 绑定属性"]
    D --> E["返回对象"]
    E --> F["调用对象方法"]

更细一点理解:

  1. User("Tom", 18) 调用类对象。
  2. Python 先创建一个空实例。
  3. Python 把这个实例作为 self 传给 __init__
  4. __init__ 给实例绑定属性。
  5. 后续 user.say_hello() 调用方法时,Python 也会自动把 user 作为 self 传入。

所以:

python
user.say_hello()

可以粗略理解成:

python
User.say_hello(user)

这就是为什么实例方法第一个参数通常写 self

对象、引用和属性

Python 变量保存的不是对象本身,而是对象引用。

python
user1 = User("Tom", 18)
user2 = user1
user2.age = 20
print(user1.age)  # 20

user1user2 指向同一个对象。

mermaid
flowchart TD
    A["变量 user1"] --> C["User对象<br/>name=Tom age=20"]
    B["变量 user2"] --> C

这和 list、dict 的可变对象问题是一致的。对象属性被修改后,所有引用这个对象的地方都会看到变化。

商业项目中要注意:

场景风险建议
多处共享同一个对象一个地方修改影响其他地方明确对象是否可变
把 ORM 实体到处传事务关闭后访问懒加载字段报错Service 内转换为 DTO
对象属性随便改状态流转不可控用方法表达状态变化

实例属性和类属性

python
class User:
    category = "normal"

    def __init__(self, name: str):
        self.name = name
类型归属示例
实例属性每个对象独有self.name
类属性类共享User.category

如果属性每个对象都不一样,放实例属性;如果所有对象共享,才考虑类属性。

类属性最容易出错的是放可变对象:

python
class ImportTask:
    errors: list[str] = []

    def add_error(self, message: str) -> None:
        self.errors.append(message)

多个实例会共享同一个 errors

python
task1 = ImportTask()
task2 = ImportTask()

task1.add_error("A001 失败")
print(task2.errors)  # ['A001 失败']

正确写法:

python
class ImportTask:
    def __init__(self) -> None:
        self.errors: list[str] = []

判断原则:

数据应该放哪里
每个用户姓名实例属性
每个任务错误列表实例属性
所有对象共享的类型名称类属性
常量配置类属性或模块常量

方法类型

python
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

python
from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int

使用:

python
user = User(name="Tom", age=18)
print(user)

dataclass 会自动生成 __init____repr__ 等方法,适合 DTO、配置对象、简单数据模型。

dataclass 适合什么

dataclass 适合字段稳定、行为不复杂的数据对象。例如资产导入结果、分页参数、统计结果。

python
from dataclasses import dataclass


@dataclass
class AssetImportResult:
    success_count: int
    failed_count: int
    error_file: str | None = None

不可变数据可以加 frozen=True

python
@dataclass(frozen=True)
class AssetQuery:
    hospital_id: int
    status: str
    page_size: int = 20

frozen=True 的意义是:查询条件创建后不应该被业务过程偷偷修改。这样更利于调试和测试。

dataclass 和 dict 怎么选

场景建议
临时处理 JSONdict
字段稳定的内部数据结构dataclass
Web 入参出参校验Pydantic Model
有复杂行为和状态流转普通类

如果一个 dict 在项目里到处传,并且大家都要记住它有哪些 key,就应该考虑换成 dataclass 或 Pydantic。

继承

继承表示“子类是一种父类”。

python
class Animal:
    def speak(self):
        print("sound")


class Dog(Animal):
    def speak(self):
        print("wang")

调用:

python
dog = Dog()
dog.speak()

继承可以复用代码,但不要滥用。继承层级太深会让代码难理解。

继承适合表达稳定的 is-a 关系:

text
Dog 是一种 Animal
CsvAssetReader 是一种 AssetReader

不适合表达“使用关系”:

text
OrderService 使用 EmailSender

OrderService 不应该继承 EmailSender,因为订单服务不是一种邮件发送器。

多态:同一个接口,不同实现

多态是面向对象里最有价值的能力之一。它让调用方只依赖统一接口,不关心具体实现。

python
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"}]

调用方:

python
def import_assets(reader: AssetReader, source: str) -> int:
    rows = reader.read(source)
    return len(rows)
mermaid
flowchart TD
    A["import_assets"] --> B["AssetReader 接口"]
    B --> C["CsvAssetReader"]
    B --> D["ApiAssetReader"]
    C --> E["读取 CSV"]
    D --> F["调用接口"]

调用方只知道 reader.read(),不关心数据来自 CSV 还是接口。新增 Excel 读取器时,新增一个类即可。

抽象类和协议

Python 可以用 abc 定义抽象类,明确子类必须实现什么方法。

python
from abc import ABC, abstractmethod


class AssetReader(ABC):
    @abstractmethod
    def read(self, source: str) -> list[dict[str, str]]:
        pass

如果子类没有实现 read,就不能实例化。

Python 也常用“鸭子类型”:只要对象有需要的方法,就可以用。类型标注里可以用 Protocol 表达这种结构化接口。

python
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只关心能力结构,降低继承耦合
普通类业务对象或服务对象

组合优先

很多场景用组合比继承更清楚。

python
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:

python
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"])

这样做的好处:

  1. AssetImportService 的依赖一眼可见。
  2. 测试时可以传入假的 Repository。
  3. 审计逻辑和存储逻辑可以独立替换。
  4. 不需要复杂继承层级。

测试时替换依赖:

python
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__支持上下文管理器

示例:

python
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,常用于资源管理。

python
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()

使用:

python
with ManagedFile("demo.txt") as file:
    content = file.read()

为什么重要:文件、数据库连接、锁、网络连接都需要释放资源。with 能保证即使中间抛异常,退出时也能执行清理逻辑。

私有属性约定

Python 没有严格的 private,但有命名约定:

写法含义
name公开属性
_name内部使用,不建议外部直接访问
__name名称改写,主要避免子类冲突

不要依赖命名做安全控制。真正的权限控制要通过业务逻辑实现。

property:控制属性读写

如果一个属性不能被随便设置,可以用 property 包一层校验。

python
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

使用:

python
order = Order(100)
order.amount = -1  # ValueError

这比到处手动校验更集中。不过也不要滥用 property,把复杂业务流程藏进属性赋值里会让代码难读。复杂状态变化更适合显式方法,例如 order.pay()order.cancel()

类设计流程

mermaid
flowchart TD
    A["识别业务对象"] --> B["列出对象属性"]
    B --> C["列出对象行为"]
    C --> D{"主要是保存数据吗"}
    D -- "是" --> E["使用 dataclass"]
    D -- "否" --> F["定义普通类"]
    F --> G{"是否存在 is-a 关系"}
    G -- "是" --> H["考虑继承"]
    G -- "否" --> I["优先组合"]

商业场景 Demo:资产导入领域模型

下面用一个完整但不复杂的例子,把 dataclass、普通类、组合、多态放在一起。

需求:

  1. 资产可以从 CSV 或接口读取。
  2. 读取后要校验资产编号和名称。
  3. 校验通过后保存。
  4. 保存后记录审计日志。

数据对象

python
from dataclasses import dataclass


@dataclass(frozen=True)
class Asset:
    asset_code: str
    asset_name: str
    dept_code: str

为什么 frozen=True:导入过程中的资产对象是一个清洗后的结果,不希望保存前被随便修改。如果要修改,应生成新对象。

读取接口

python
from typing import Protocol


class AssetReader(Protocol):
    def read(self, source: str) -> list[Asset]:
        ...

CSV 实现

python
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 实现

python
class ApiAssetReader:
    def read(self, source: str) -> list[Asset]:
        return [
            Asset(asset_code="B001", asset_name="DR", dept_code="D03"),
        ]

Repository 和审计

python
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 编排

python
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("资产名称不能为空")

使用

python
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 私有约定校验逻辑收在服务内部

流程:

mermaid
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 项目里常见分层:

text
api -> service -> repository/client -> database/external api

对应对象:

常见对象职责
APIRouter、Schema接收请求、参数校验、返回响应
ServiceAssetImportService业务编排、事务边界、规则判断
RepositoryAssetRepository数据库访问
ClientHospitalApiClient外部接口访问
DTO/Modeldataclass / Pydantic表达数据结构

不要让一个类横跨所有层。例如 AssetController 既读文件、又校验、又写库、又调外部接口,这会让代码很难维护。

生产排查:类设计问题怎么发现

mermaid
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 面向对象怎么理解

text
面向对象是把相关数据和行为封装到对象里,让代码更贴近业务概念。类是对象模板,对象是类创建出来的实例。实例属性保存每个对象自己的状态,方法表达对象行为。Python 中面向对象常用于领域模型、Service、Repository、Client、配置对象和 DTO。简单逻辑用函数即可,数据和行为关系复杂时再用类。

继承和组合怎么选

text
继承适合稳定的 is-a 关系,比如 CsvReader 是一种 Reader;组合适合使用关系,比如 AssetImportService 使用 Repository 和 AuditLogger。商业项目里优先组合,因为组合依赖更清晰、测试更容易、继承层级更浅。继承层级太深会让父类修改影响很多子类。

dataclass 适合什么

text
dataclass 适合字段稳定、行为不复杂的数据对象,例如查询条件、导入结果、DTO、配置快照。它能自动生成 __init__、__repr__ 等方法,减少模板代码。如果对象需要复杂状态流转和行为,应该用普通类;如果是 Web 入参出参校验,常用 Pydantic。

Python 多态怎么实现

text
Python 多态可以通过继承、抽象类或 Protocol 实现。调用方只依赖统一方法,例如 reader.read(),具体对象可以是 CsvReader、ApiReader 或 ExcelReader。Python 也支持鸭子类型,只要对象有需要的方法就能使用。类型标注中可以用 Protocol 表达这种结构化接口。

为什么要构造函数注入依赖

text
构造函数注入能让类依赖一眼可见,也方便测试时替换成 fake 或 mock。如果类内部自己创建数据库连接、HTTP 客户端或 Repository,测试会被真实外部依赖绑住,代码也更难扩展。组合加构造函数注入是 Python 项目工程化的重要习惯。

练习

  1. 定义一个 User 类,包含 name、age 和 is_adult() 方法。
  2. dataclass 定义一个 Product 数据对象。
  3. 写一个 Cart 类,支持添加商品和计算数量。
  4. 用组合实现 OrderService 调用 EmailSender
  5. 思考“狗继承动物”和“订单服务使用邮件服务”哪个适合继承,哪个适合组合。

小结

面向对象不是为了把所有代码都写成类,而是为了在数据和行为关系复杂时保持结构清晰。初学者重点掌握类、对象、self、实例属性、方法、dataclass、继承和组合。能用组合就不要强行继承,能用简单函数就不要硬造类。