Skip to content

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、出版年份等)。

使用面向过程的方式:

python
# 每本图书需要多个变量
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 实际代码):

python
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

python
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类属性定义在方法外,所有对象共享

创建和使用对象

python
# 实例化 — 传入参数,自动调用 __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 实例属性

python
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.pydemo_ch01() 演示:

python
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

验证方式:

bash
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__ 中初始化对象创建后状态完整,避免 AttributeErrorself.title = title
类属性用不可变类型避免多实例意外修改共享数据total_books: int = 0
使用类型注解提高可读性,便于 IDE 检查self.title: str = title
类属性通过类名访问语义更清晰,避免实例遮蔽BookItem.total_books
from __future__ import annotations启用延迟注解,支持前向引用文件首行

反模式:不要这样做

python
# ❌ 属性在 __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
python
# 验证:__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.attrO(1) 均摊通过 __dict__ 哈希查找
访问类属性 BookItem.attrO(1)直接字典查找
创建 10000 个实例按属性数量线性增长__slots__ 可减少约 40%-50% 内存
python
# 验证:属性查找通过 __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 章   │
              │   继承      │      │   封装      │        │  属性与方法  │
              └─────────────┘      └─────────────┘        └─────────────┘

自检清单

回答以下问题,检验是否掌握本章内容:

  1. 类和对象的区别是什么? → 类是模板/蓝图,对象是类的具体实例。一个类可以创建多个对象。

  2. __init__ 方法的作用是什么?它在什么时候被调用? → 初始化对象的属性。在实例化(BookItem(...))时自动调用,无需手动调用。

  3. self 参数的作用是什么?可以省略吗? → 指向当前实例对象,用于访问实例属性和方法。不能省略,Python 需要它来区分不同实例的数据。

  4. 实例属性和类属性有什么区别? → 实例属性(self.xxx)每个对象独立拥有;类属性(定义在方法外)所有对象共享。

  5. 为什么说面向对象比面向过程更适合管理复杂数据? → OOP 将数据和行为封装在一起,通过类模板可以创建任意数量的对象,避免变量爆炸,代码更易维护和扩展。


本章能力清单

  • [x] 理解类与对象的关系(模板与实例)
  • [x] 使用 class 关键字定义类
  • [x] 编写 __init__ 方法初始化实例属性
  • [x] 理解 self 的含义并正确使用
  • [x] 区分实例属性和类属性
  • [x] 创建和使用对象(实例化、访问属性、调用方法)
  • [x] 遵循 OOP 最佳实践(命名规范、属性初始化位置)
  • [x] 识别并避免常见陷阱(可变类属性、动态添加属性)

⭐ 选读提示:你可能好奇 BookItem(...) 背后发生了什么——__new__ 负责创建对象(分配内存),__init__ 负责初始化属性。详细内容见 第 9 章(元类),那里会深入讲解对象创建流程和单例模式的实现。