01-面向对象基础
Python 版本要求:Python 3.11+ 贯穿项目:oop_demo/ 代码位置:
oop_demo/app/domain/book/model.py(BookItem 类) 测试验证:cd oop_demo && uv run pytest -k TestBookItemBasics -v交互式演示:cd oop_demo && uv run python -m app→ 选 1
概念铺垫
为什么需要面向对象?
问题场景(面向过程 vs 面向对象)
假设你要开发一个图书馆管理系统,需要管理大量图书信息(书名、作者、ISBN、出版年份等)。
使用面向过程的方式:
# 每本图书需要多个变量
book1_title = "流畅的Python"
book1_author = "Ramalho"
book1_isbn = "9781491946008"
book1_year = 2015
book1_available = True
book2_title = "设计模式"
book2_author = "GoF"
book2_isbn = "9780201633610"
book2_year = 1994
book2_available = True
def print_book(title: str, author: str, isbn: str, year: int) -> None:
print(f"《{title}》{author} ({year}) ISBN:{isbn}")
print_book(book1_title, book1_author, book1_isbn, book1_year)
print_book(book2_title, book2_author, book2_isbn, book2_year)问题:
- 图书数量增加时,变量数量爆炸
- 相关数据没有组织在一起,容易传参出错
- 借阅状态(
available)与图书数据分离,难以维护
使用面向对象的方式(oop_demo 实际代码):
from app.domain.book.model import BookItem
book1 = BookItem("流畅的Python", "Ramalho", "9781491946008", 2015)
book2 = BookItem("设计模式", "GoF", "9780201633610", 1994)
print(book1) # 《流畅的Python》Ramalho(可借)
print(book2.title) # 设计模式
book1.checkout() # 借出
print(book1.is_available()) # False优势:
- 数据和行为封装在一个对象中
- 通过类可以创建任意数量的图书对象
- 代码更易维护、扩展和测试
生活类比:建筑图纸与房子
| 概念 | 类比 | 说明 |
|---|---|---|
| 类(Class) | 建筑图纸 | 定义房子的结构、房间数量、门窗位置 |
| 对象(Object) | 按图纸建好的房子 | 每一栋房子都是独立的,有自己的门牌号、住户 |
__init__ | 交房时的初始配置 | 每栋房子交房时刷不同的颜色、装不同的家具 |
self | "我的房子" | 区分"我家的大门"和"你家的大门" |
一栋图纸可以建无数栋房子,每栋房子互不影响——这就是类和对象的关系。
L1 理解层:会用
类的定义
来自 oop_demo/app/domain/book/model.py:
class BookItem(Catalogable):
"""图书基类 — 所有图书类型的抽象基础
核心职责:
- 唯一标识(ISBN)
- 借阅状态管理
- 编目信息展示
"""
total_books: int = 0 # 类属性:所有实例共享
def __init__(self, title: str, author: str, isbn: str, year: int) -> None:
"""初始化方法 — 创建对象时自动调用"""
self.title: str = title # 实例属性:每个对象独立
self.author: str = author
self._isbn: str = isbn
self._year: int = year
self._available: bool = True
BookItem.total_books += 1 # 每创建一本,计数器 +1关键要素:
| 要素 | 作用 | 示例 |
|---|---|---|
class BookItem | 定义类,大驼峰命名 | 类名用 PascalCase |
__init__ | 构造时初始化属性 | 自动调用,无需手动调用 |
self | 指向当前实例 | 区分 self.title(我的)和 other.title(别人的) |
self.xxx | 实例属性 | 每个对象独立拥有一份 |
total_books | 类属性 | 定义在方法外,所有对象共享 |
创建和使用对象
# 实例化 — 传入参数,自动调用 __init__
book = BookItem("流畅的Python", "Ramalho", "9781491946008", 2015)
# 访问实例属性
print(book.title) # 流畅的Python
print(book.author) # Ramalho
print(book.year) # 2015(通过 @property 访问 _year)
# 调用方法
print(book.is_available()) # True
book.checkout() # 借出
print(book.is_available()) # False
book.return_item() # 归还
print(book.is_available()) # True
# 查看编目信息
print(book.get_info())
# [9781491946008] 流畅的Python — Ramalho (2015)类属性 vs 实例属性
BookItem.total_books = 0 # 重置计数器
b1 = BookItem("Python", "Guido", "9780000000001", 2020)
b2 = BookItem("Java", "Gosling", "9780000000002", 2021)
# 实例属性:每个对象独立
print(b1.title) # Python
print(b2.title) # Java
# 类属性:所有对象共享
print(b1.total_books) # 2
print(b2.total_books) # 2(同一个值)
print(BookItem.total_books) # 2(通过类名访问更清晰)贯穿实战:图书馆图书管理
来自 oop_demo/app/main.py 的 demo_ch01() 演示:
from app.domain.book.model import BookItem
# 重置计数器(演示用)
BookItem.total_books = 0
# 创建图书对象
book = BookItem("流畅的Python", "Ramalho", "9781491946008", 2015)
print(f"创建图书: {book}")
print(f" 书名: {book.title}")
print(f" 作者: {book.author}")
print(f" ISBN: {book.isbn}")
print(f" 年份: {book.year}")
print(f" 可借: {book.is_available()}")
# 创建多个对象,统一管理
books = [
BookItem("设计模式", "GoF", "9780201633610", 1994),
BookItem("代码大全", "McConnell", "9780735619678", 2004),
]
for b in books:
print(f" + {b.title}")
print(f"\n图书创建总数: {BookItem.get_total()}") # 3验证方式:
cd oop_demo && uv run pytest -k TestBookItemBasics -v测试覆盖:
test_instance_attributes— 实例属性正确初始化test_class_attribute_increments— 类属性计数器递增test_classmethod_get_total— 类方法返回总数test_staticmethod_validate_isbn— 静态方法校验 ISBN 格式
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 类名使用大驼峰 | 符合 PEP8,与函数名区分 | class BookItem: |
所有属性在 __init__ 中初始化 | 对象创建后状态完整,避免 AttributeError | self.title = title |
| 类属性用不可变类型 | 避免多实例意外修改共享数据 | total_books: int = 0 |
| 使用类型注解 | 提高可读性,便于 IDE 检查 | self.title: str = title |
| 类属性通过类名访问 | 语义更清晰,避免实例遮蔽 | BookItem.total_books |
from __future__ import annotations | 启用延迟注解,支持前向引用 | 文件首行 |
反模式:不要这样做
# ❌ 属性在 __init__ 外部动态添加
class BookItem:
def set_title(self, title: str) -> None:
self.title = title # 可能导致 AttributeError
b = BookItem()
print(b.title) # AttributeError!
# ✅ 所有属性在 __init__ 中初始化
class BookItem:
def __init__(self, title: str = "") -> None:
self.title: str = title
# ❌ 可变对象作为类属性
class Config:
books: list[str] = [] # 所有实例共享同一个列表!
# ✅ 类属性用不可变类型,可变数据放实例属性
class Config:
max_books: int = 100 # 不可变
def __init__(self) -> None:
self.books: list[str] = [] # 每个实例独立适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 管理多个相同结构的数据 | 推荐 | 类作为模板,批量创建实例 |
| 只有一两个函数的小脚本 | 不推荐 | OOP 带来不必要的复杂度 |
| 数据结构变化频繁 | 谨慎 | 用 __slots__ 或 @dataclass 控制属性 |
| 数据与行为分离的场景 | 推荐 | OOP 的核心优势就是封装数据和行为 |
L3 专家层:深入
Python 如何实现:对象创建全流程
当执行 BookItem("流畅的Python", "Ramalho", "9781491946008", 2015) 时,CPython 内部实际发生的步骤:
BookItem("流畅的Python", "Ramalho", "9781491946008", 2015)
│
▼
┌────────────────────────────────────────────────┐
│ type.__call__(BookItem, "流畅的Python", ...) │ ← 元类的 __call__ 被触发
│ │
│ def __call__(cls, *args, **kwargs): │
│ obj = cls.__new__(cls, *args, **kwargs) │ ← 步骤1: 分配内存
│ if isinstance(obj, cls): │
│ obj.__init__(*args, **kwargs) │ ← 步骤2: 初始化属性
│ return obj │
└────────────────────────────────────────────────┘
│
▼
┌──────────────┐
│ 实例对象 │
│ __dict__ │
└──────────────┘关键区别:__new__ vs __init__
| 方法 | 职责 | 返回 | 调用顺序 |
|---|---|---|---|
__new__ | 创建并返回实例对象(分配内存) | 实例对象 | 先 |
__init__ | 初始化实例属性 | None | 后 |
# 验证:__new__ 和 __init__ 的调用顺序
class Demo:
def __new__(cls, *args, **kwargs):
print(f"1. __new__ 被调用,创建对象")
obj = super().__new__(cls)
print(f" 对象已创建: {obj}")
return obj
def __init__(self, name):
print(f"2. __init__ 被调用,初始化 {name}")
self.name = name
d = Demo("test")
# 1. __new__ 被调用,创建对象
# 对象已创建: <__main__.Demo object at 0x...>
# 2. __init__ 被调用,初始化 test__init__ 为什么返回 None?
这是 Python 的设计约束:如果 __init__ 返回非 None 值,会抛出 TypeError: __init__() should return None。原因在于 type.__call__ 中,Python 始终返回 __new__ 创建的对象,__init__ 只负责初始化,不负责创建。
性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
实例化 BookItem(...) | O(1) + __init__ 开销 | __new__ 是 O(1),__init__ 随属性数量线性增长 |
访问实例属性 obj.attr | O(1) 均摊 | 通过 __dict__ 哈希查找 |
访问类属性 BookItem.attr | O(1) | 直接字典查找 |
| 创建 10000 个实例 | 按属性数量线性增长 | __slots__ 可减少约 40%-50% 内存 |
# 验证:属性查找通过 __dict__
b = BookItem("Python", "Guido", "9780000000001", 2020)
print(b.__dict__)
# {'title': 'Python', 'author': 'Guido', '_isbn': '9780000000001',
# '_year': 2020, '_available': True}
# __dict__ 是一个 dict,属性访问本质是 __dict__["key"] 查找
print(type(b.__dict__)) # <class 'dict'>知识关联
本章知识关联:
┌───────────────┐
│ type 元类 │ ← 第 9 章
│ 创建类对象 │
└───────┬───────┘
│
↓
┌─────────────┐ ┌───────────────┐ ┌─────────────┐
│ __new__ │→│ __init__ │→│ 实例对象 │
│ 创建对象 │ │ 初始化属性 │ │ __dict__ │
└─────────────┘ └───────────────┘ └──────┬──────┘
│
┌──────────────────────┼──────────────────────┐
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 类属性 │ │ 实例属性 │ │ 实例方法 │
│ total_books │ │ title/author│ │ checkout() │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ @classmethod │ │ @property │ │@staticmethod│
│ get_total() │ │ isbn/year │ │validate_isbn│
└─────────────┘ └─────────────┘ └─────────────┘
│
↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 第 3 章 │ │ 第 4 章 │ │ 第 2 章 │
│ 继承 │ │ 封装 │ │ 属性与方法 │
└─────────────┘ └─────────────┘ └─────────────┘自检清单
回答以下问题,检验是否掌握本章内容:
类和对象的区别是什么? → 类是模板/蓝图,对象是类的具体实例。一个类可以创建多个对象。
__init__方法的作用是什么?它在什么时候被调用? → 初始化对象的属性。在实例化(BookItem(...))时自动调用,无需手动调用。self参数的作用是什么?可以省略吗? → 指向当前实例对象,用于访问实例属性和方法。不能省略,Python 需要它来区分不同实例的数据。实例属性和类属性有什么区别? → 实例属性(
self.xxx)每个对象独立拥有;类属性(定义在方法外)所有对象共享。为什么说面向对象比面向过程更适合管理复杂数据? → OOP 将数据和行为封装在一起,通过类模板可以创建任意数量的对象,避免变量爆炸,代码更易维护和扩展。
本章能力清单
- [x] 理解类与对象的关系(模板与实例)
- [x] 使用
class关键字定义类 - [x] 编写
__init__方法初始化实例属性 - [x] 理解
self的含义并正确使用 - [x] 区分实例属性和类属性
- [x] 创建和使用对象(实例化、访问属性、调用方法)
- [x] 遵循 OOP 最佳实践(命名规范、属性初始化位置)
- [x] 识别并避免常见陷阱(可变类属性、动态添加属性)
⭐ 选读提示:你可能好奇
BookItem(...)背后发生了什么——__new__负责创建对象(分配内存),__init__负责初始化属性。详细内容见 第 9 章(元类),那里会深入讲解对象创建流程和单例模式的实现。