【Python】面向对象编程(第三阶段)
三、面向对象编程
阶段定位:面向对象编程(OOP)是 Python 组织大型项目的核心范式。Python 的 OOP 灵活而不教条——它支持封装、继承、多态等经典特性,同时通过鸭子类型和魔术方法提供了独特的表达能力。本阶段不仅要学会”怎么写类”,更要理解”为什么这样设计”。
1 | ┌─────────────────────────────────────────────────────────────┐ |
1. 类与对象
1.1 类的定义与实例化
面向对象的三大支柱:
1 | ┌──────────┐ ┌──────────┐ ┌──────────┐ |
1 | class Person: |
类属性 vs 实例属性的内存模型:
1 | Person 类对象 |
1 | # 类属性与实例属性的区别 |
属性查找顺序:实例属性 → 类属性 → 父类属性(MRO 顺序)
1 | 查找 alice.species 的过程: |
面试真题:以下代码输出什么?
1
2
3
4
5
6
7
8
9
10
11 class Test:
items = []
def add(self, x):
self.items.append(x)
t1 = Test()
t2 = Test()
t1.add(1)
t2.add(2)
print(t1.items) # [1, 2] ← 因为 items 是类属性,所有实例共享同一个列表!
print(t2.items) # [1, 2]解析:
self.items.append(x)查找items时,实例__dict__没有,找到类属性。所有实例操作的是同一个列表。这与可变默认参数陷阱是同一类问题。
易错点:在实例上”修改”类属性时,实际上是创建了同名实例属性,不会影响类属性和其他实例:
1
2
3
4 alice.species = "Alien" # 创建实例属性,不影响 Person.species
Person.species = "Robot" # 修改类属性,影响所有没有实例属性的实例
print(alice.species) # 仍然是 "Alien"(实例属性优先)
print(bob.species) # "Robot"(读取新的类属性)
1.2 self 的本质
self 不是关键字,而是一个约定俗成的参数名。它代表调用该方法的实例对象本身。
1 | class Demo: |
Python 方法绑定的底层原理:
1 | class MyClass: |
面试真题:
self可以改成别的名字吗?
1
2
3
4
5 class Test:
def __init__(this, x):
this.x = x
def show(me):
print(me.x)答案:可以运行,但强烈不推荐。PEP 8 规范要求使用
self作为实例方法第一个参数名,cls作为类方法第一个参数名。
1.3 __init__ vs __new__
对象创建的完整流程:
1 | 执行 Person("Alice", 30) 时的内部流程: |
1 | class Singleton: |
关键区别:
| 方法 | 职责 | 返回值 | 调用时机 | 是否常重写 |
|---|---|---|---|---|
__new__(cls) |
创建实例对象 | 必须返回实例 | 在 __init__ 之前 |
很少重写 |
__init__(self) |
初始化实例属性 | 必须返回 None |
在 __new__ 之后 |
经常重写 |
易错点:单例模式中
__init__每次都会被调用!
1
2
3
4 s1 = Singleton(1)
s2 = Singleton(2) # __init__ 再次执行,value 被覆盖为 2
print(s1 is s2) # True
print(s1.value) # 2 ← 被覆盖了!修复方法:
1
2
3
4 def __init__(self, value):
if not hasattr(self, '_initialized'):
self.value = value
self._initialized = True
面试真题:
__new__返回的不是cls的实例会怎样?
1
2
3
4
5
6
7 class Weird:
def __new__(cls, *args, **kwargs):
return "I'm a string, not an instance"
w = Weird()
print(w) # "I'm a string, not an instance"
print(type(w)) # <class 'str'> —— __init__ 不会被调用!
工程建议:99% 的场景不需要重写 __new__,只在以下场景使用:
- 单例模式
- 不可变类型子类化(如
str、int、tuple子类) - 自定义元类中控制类创建
1.4 __slots__:限制实例属性
默认情况下,Python 实例可以动态添加任意属性。使用 __slots__ 可以限制允许的属性,同时大幅减少内存占用。
1 | class Point: |
__slots__ 的内存对比:
1 | 普通类实例: |
| 特性 | 普通类 | __slots__ 类 |
|---|---|---|
| 内存占用 | 大(每个实例有 __dict__) |
小(直接存储) |
| 动态属性 | 支持 | 不支持 |
| 属性速度 | 慢(字典查找) | 快(描述符直接访问) |
| 继承 | 正常 | 子类也需定义 __slots__ |
| 序列化 | 直接 __dict__ |
需手动处理 |
工程建议:当你需要创建大量实例(如百万级数据对象)时,使用
__slots__可以显著节省内存。但如果项目规模不大,不需要为了优化而使用。
2. 类方法、静态方法、实例方法
1 | class DateUtils: |
三种方法对比决策树:
1 | 需要访问实例属性(self)? |
工厂方法实战:
1 | class Date: |
面试真题:
@staticmethod和在类外面定义普通函数有什么区别?
1
2
3
4
5
6
7 class MathUtils:
def square(x):
return x ** 2
def square_outside(x):
return x ** 2答案:功能上没有区别。
@staticmethod只是逻辑分组——告诉使用者”这个函数和这个类有关”。调用方式上MathUtils.square(5)比全局square_outside(5)更有组织性。如果你的工具函数和某个类概念上相关,就放在类里作为静态方法;否则直接放模块级。
工程建议:当你发现自己写了多个创建对象的替代方式时,用
@classmethod做工厂方法,而不是在__init__中写一堆if-else分支。
3. 封装与访问控制
3.1 Python 的”信任”哲学
Python vs Java/C++ 的访问控制对比:
1 | Java/C++ 的访问控制(强制): |
1 | class BankAccount: |
名称改写(Name Mangling)的原理:
1 | # Python 解释器在编译类定义时,会将 __attr 改写为 _ClassName__attr |
面试真题:
_name和__name有什么区别?
_name:纯约定,Python 不做任何特殊处理,外部可以访问__name:触发名称改写,变为_ClassName__name,间接”私有化”__name__(双下划线开头和结尾):Python 魔术方法/属性,不要自己发明
工程建议:Python 社区的最佳实践是:
- 默认用公开属性(
name),因为 Python 没有真正的私有- 需要内部标记时用
_name- 只在确实需要防止子类覆盖时用
__name- 不要用
__double_leading_and_trailing__,那是 Python 保留的
3.2 @property:Pythonic 的 getter/setter
1 | class Temperature: |
property 的完整生命周期:
1 | ┌────────────────────────────────────────────────┐ |
property 的优势:
- 对外暴露像属性一样访问,内部包含验证/计算逻辑
- 后续可以给已有属性添加验证,而不破坏外部 API
- 配合
@deleter可控制删除行为
1 | class User: |
平滑升级示例:
1 | # 版本 1:直接公开属性 |
易错点:在
__init__中给self.price赋值时,会触发@price.setter!
1 p = Product(-10) # ValueError: 价格不能为负
工程建议:不要一开始就给所有属性加
@property。先用简单公开属性,需要验证时再用 property 替换——这就是 Python 的”渐进式封装”哲学。
3.3 __slots__ 与内存优化
当需要创建大量实例时,__slots__ 可以显著节省内存:
1 | import sys |
4. 继承与多态
4.1 单继承与多态
继承的 UML 类图:
1 | ┌──────────┐ |
1 | class Animal: |
多态的本质:
1 | 多态不是"检查类型",而是"检查行为": |
4.2 多继承与 MRO(方法解析顺序)
菱形继承结构:
1 | ┌───┐ |
1 | class A: |
C3 线性化算法的核心规则:
1 | 1. 子类优先于父类 |
面试真题:以下代码的 MRO 是什么?
1
2
3
4
5
6 class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
print(C.__mro__)答案:会抛出
TypeError: Cannot create a consistent method resolution。因为 A 要求 X 在 Y 前面,B 要求 Y 在 X 前面,C 继承 A 和 B 时产生矛盾,C3 算法无法线性化。
工程建议:Python 的多继承虽然强大,但过度使用会导致继承链混乱。最佳实践是:
- 主继承链用单继承
- 额外功能用 Mixin 模式混入
- 接口约束用 ABC
- 如果继承超过 3 层,考虑用组合替代继承
4.3 super() 的使用
1 | class Parent: |
super() 在多继承中的行为:
1 | super() 不是"调用父类",而是"按 MRO 顺序调用下一个类" |
1 | class MixinA: |
易错点:
super().__init__()在多继承中可能会跳过某些父类的__init__!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 class A:
def __init__(self):
print("A.__init__")
super().__init__()
class B:
def __init__(self):
print("B.__init__")
super().__init__()
class C(A, B):
def __init__(self):
print("C.__init__")
super().__init__()
C()
# C.__init__ → A.__init__ → B.__init__ → object.__init__
# 如果 A 的 __init__ 没有调用 super().__init__(),B 的 __init__ 会被跳过!结论:多继承时,所有类都必须调用
super().__init__(),否则 MRO 链条会断裂。
4.4 Mixin 模式
Mixin 的设计原则:
1 | ┌────────────────────────────────────────────────────────────┐ |
1 | class JsonMixin: |
工程建议:Mixin 是 Python 中实现”组合优于继承”的方式。当你需要给多个不相关的类添加相同功能(如日志、缓存、序列化)时,用 Mixin 比用继承更灵活。
4.5 鸭子类型与抽象基类
鸭子类型 vs ABC 决策:
1 | 需要接口约束? |
鸭子类型:
1 | class Duck: |
抽象基类(ABC):
1 | from abc import ABC, abstractmethod |
鸭子类型 vs ABC:
| 特性 | 鸭子类型 | 抽象基类 |
|---|---|---|
| 检查方式 | 运行时 | 可提前(实例化时) |
| 灵活性 | 高 | 低(必须实现所有抽象方法) |
| 接口保证 | 无 | 有 |
| Pythonic 程度 | 更 Pythonic | 更”工程化” |
| IDE 支持 | 差 | 好 |
| 适用场景 | 脚本、快速开发 | 大型项目、团队协作 |
4.6 typing.Protocol:结构化类型(3.8+)
1 | from typing import Protocol |
总结:
Protocol结合了鸭子类型的灵活性和静态类型检查的安全性。在大型项目中推荐使用。
5. 魔术方法(协议)
魔术方法全景图:
1 | ┌─────────────────────────────────────────────────────────────┐ |
5.1 表示方法:__str__ 与 __repr__
1 | class Point: |
__str__ vs __repr__ 调用时机:
1 | ┌──────────────────────────────────────────────────────┐ |
易错点:列表打印时,元素使用的是
__repr__而非__str__:
1
2
3
4 points = [Point(1, 2), Point(3, 4)]
print(points)
# [Point(x=1, y=2), Point(x=3, y=4)] ← 使用 __repr__
# 不是 [(1, 2), (3, 4)]
工程建议:至少实现
__repr__,因为:
- 调试时能快速看到对象状态
__str__缺失时自动回退到__repr____repr__应返回可执行的代码(如Point(x=3, y=4)),方便eval重建
5.2 容器协议
1 | class Team: |
容器协议方法与内置函数的映射:
1 | ┌──────────────────┬─────────────────────────────────┐ |
5.3 上下文管理器协议
with 语句的执行流程:
1 | with resource as r: |
1 | class DatabaseConnection: |
使用 contextlib 简化上下文管理器:
1 | from contextlib import contextmanager |
工程建议:需要资源管理(文件、数据库连接、锁)时,优先使用上下文管理器。简单场景用
@contextmanager,复杂场景用__enter__/__exit__。
5.4 可调用对象:__call__
1 | class LRUCache: |
__call__ 的常见用途:
1 | ┌──────────────────────┬──────────────────────────────────┐ |
面试真题:
obj()和obj.__call__()有什么区别?
1
2
3
4
5
6
7
8 class A:
def __call__(self):
return "called"
a = A()
print(a()) # "called" —— 语法糖
print(a.__call__()) # "called" —— 直接调用
# 两者等价,但推荐 a(),更简洁可读
5.5 运算符重载
1 | class Vector: |
运算符查找顺序:
1 | v1 + v2 的查找过程: |
工程建议:
- 运算符重载方法遇到不支持的类型时,返回
NotImplemented(不是TypeError),让 Python 尝试反向运算符- 实现
__eq__时,也要考虑__hash__(见 5.6)- 实现
__lt__等比较运算符后,可以用@functools.total_ordering自动补全其他比较方法
@functools.total_ordering 自动补全比较方法:
1 | from functools import total_ordering |
常用魔术方法速查:
| 协议 | 方法 | 触发方式 |
|---|---|---|
| 构造 | __init__ |
obj = Class() |
| 表示 | __str__ / __repr__ |
str(obj) / repr(obj) |
| 长度 | __len__ |
len(obj) |
| 索引 | __getitem__ / __setitem__ |
obj[key] / obj[key] = v |
| 迭代 | __iter__ / __next__ |
for x in obj |
| 包含 | __contains__ |
x in obj |
| 上下文 | __enter__ / __exit__ |
with obj as x: |
| 可调用 | __call__ |
obj() |
| 比较 | __eq__ / __lt__ / __gt__ 等 |
== / < / > |
| 哈希 | __hash__ |
hash(obj) / set() 成员 |
| 布尔 | __bool__ |
bool(obj) / if obj: |
| 算术 | __add__ / __sub__ / __mul__ 等 |
+ / - / * |
| 反向算术 | __radd__ / __rmul__ 等 |
其他类型 + obj |
| 增量赋值 | __iadd__ / __isub__ 等 |
+= / -= |
| 一元 | __neg__ / __pos__ / __abs__ |
-obj / +obj / abs(obj) |
| 切片 | __getitem__(slice) |
obj[a:b] |
| 格式化 | __format__ |
f"{obj:spec}" |
5.6 __hash__ 与 __eq__ 组合(面试高频)
__hash__ 和 __eq__ 共同决定了对象在字典、集合等哈希集合中的行为。
哈希表工作原理:
1 | ┌─────────────────────────────────────────────────────────────┐ |
1 | # 默认行为:对象基于 id 比较和哈希 |
易错点:修改了参与哈希计算的属性后,对象在集合中的位置会”丢失”:
1
2
3
4 p = Person("Alice")
s = {p}
p.name = "Bob" # 修改了参与哈希的属性
print(p in s) # False!因为哈希值变了,找不到原来的桶
6. 元编程与高级 OOP
6.1 元类(Metaclass)
元类层级关系:
1 | ┌──────────────────────────────────────────────┐ |
1 | # 类的本质:type 是默认的元类 |
自定义元类:
1 | class SingletonMeta(type): |
元类实战:自动注册子类:
1 | class PluginRegistry(type): |
__init_subclass__ 替代元类(3.6+):
1 | # 3.6+ 的 __init_subclass__ 常可替代元类,更简单 |
工程建议:元类是 Python 中最强大的特性之一,也是最容易被滥用的。优先级:
- 能用普通类解决 → 不用元类
- 能用
@property/ 描述符 → 不用元类- 能用类装饰器 → 不用元类
- 能用
__init_subclass__→ 不用元类- 以上都不行 → 才考虑元类
6.2 描述符协议
描述符是 @property 的底层实现:
1 | ┌────────────────────────────────────────────────────────────┐ |
1 | class ValidatedField: |
面试真题:
@property和描述符有什么关系?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 # @property 本质上就是创建了一个数据描述符
class C:
def x(self):
return self._x
def x(self, value):
self._x = value
# 等价于:
class Property:
def __get__(self, obj, owner):
return obj._x
def __set__(self, obj, value):
obj._x = value
class C:
x = Property()
6.3 数据类 dataclass(3.7+)
dataclass 自动生成 __init__、__repr__、__eq__ 等方法,减少样板代码。
1 | from dataclasses import dataclass, field, asdict, astuple |
dataclass 常用参数:
| 参数 | 说明 |
|---|---|
init=True |
自动生成 __init__ |
repr=True |
自动生成 __repr__ |
eq=True |
自动生成 __eq__ |
order=False |
自动生成 __lt__、__le__、__gt__、__ge__ |
frozen=False |
设置为 True 使实例不可变(类似命名元组) |
slots=False |
3.10+,使用 __slots__ 优化内存 |
不可变 dataclass:
1 |
|
6.4 Enum 枚举类(3.4+)
1 | from enum import Enum, auto |
工程建议:用
Enum替代魔术数字和字符串常量,代码更可读、更安全。
7. 设计模式在 Python 中的实践
7.1 单例模式
三种实现方式对比:
1 | # 方式一:使用 __new__(见 1.3 节示例) |
| 方式 | 优点 | 缺点 |
|---|---|---|
__new__ |
简单 | __init__ 重复调用 |
| 模块级 | 最简单、最 Pythonic | 需要单独模块文件 |
| 元类 | 最干净、__init__ 只调用一次 |
理解成本高 |
| 装饰器 | 灵活 | 改变了类类型 |
工程建议:Python 中最推荐的方式是模块级单例——把状态放在模块的全局变量中,通过函数访问。这是 Django、Flask 等框架的做法。
7.2 工厂模式
1 | class PaymentProcessor: |
7.3 策略模式
1 | class DiscountStrategy: |
策略模式的 Pythonic 写法(用函数替代类):
1 | # Python 中函数是一等公民,策略模式可以更简洁 |
7.4 观察者模式
1 | class EventEmitter: |
Python vs Java/C++ OOP 对比
很多 Python 学习者来自 Java 或 C++ 背景,了解它们的差异有助于快速建立 Pythonic 的 OOP 思维。
核心差异对比表
| 特性 | Python | Java | C++ |
|---|---|---|---|
| 访问控制 | 约定(_var、__var),无强制 |
private/protected/public 关键字 |
private/protected/public 关键字 |
| 多重继承 | ✅ 直接支持 | ❌ 不支持(接口可多继承) | ✅ 支持 |
| 抽象类 | ABC 模块 + @abstractmethod |
abstract 关键字 |
纯虚函数 = 0 |
| 接口 | 无关键字,用抽象类或协议 | interface 关键字 |
纯虚类 |
| 构造函数 | __init__(初始化)+ __new__(创建) |
构造方法与类同名 | 构造函数与类同名 |
| 析构函数 | __del__(不推荐依赖) |
finalize()(已弃用) |
析构函数 ~ClassName() |
| 方法重写 | 无需关键字,直接重定义 | 必须加 @Override 注解 |
override 关键字(C++11) |
| 运算符重载 | ✅ __add__、__eq__ 等 |
❌ 不支持 | ✅ 运算符函数 |
| 属性访问 | @property 装饰器 |
get/set 方法或 Lombok |
手动编写 getter/setter |
| 元编程 | 元类、__init_subclass__ |
注解 + 反射 | 模板元编程 |
| 数据类 | @dataclass(自动生成方法) |
record(Java 14+) |
无内置支持 |
| 鸭子类型 | ✅ 核心特性 | ❌ 强类型 | 部分支持(模板) |
访问控制对比
Java/C++:强制访问控制
1 | // Java |
Python:约定式访问控制
1 | # Python |
多重继承对比
Java:接口多继承
1 | // Java - 只能继承一个类,但可实现多个接口 |
Python:真·多重继承 + MRO
1 | # Python - 可直接多继承 |
运算符重载对比
Java:不支持运算符重载
1 | // Java - 必须使用方法 |
Python:运算符重载
1 | # Python - 可以重载运算符 |
设计哲学差异
| 方面 | Python | Java/C++ |
|---|---|---|
| 核心理念 | “We are all consenting adults” | “安全优于灵活” |
| 类型检查 | 运行时(动态类型) | 编译时(静态类型) |
| 隐式行为 | 魔术方法(协议) | 显式关键字 |
| 复杂性 | 简单优先 | 严格控制 |
| 错误发现 | 运行时异常 | 编译时错误 |
常见面试题汇总
Q1:__new__ 和 __init__ 的区别?
答案:
__new__:类方法,负责创建对象实例(分配内存),返回实例__init__:实例方法,负责初始化对象属性,不返回值
1 | class Singleton: |
Q2:Python 的 MRO 是什么?如何计算?
答案:MRO(Method Resolution Order)是方法解析顺序,使用 C3 线性化算法计算。
1 | class A: pass |
Q3:@classmethod、@staticmethod 和实例方法的区别?
答案:
| 类型 | 第一个参数 | 调用方式 | 用途 |
|---|---|---|---|
| 实例方法 | self(实例) |
obj.method() |
操作实例数据 |
| 类方法 | cls(类) |
Class.method() 或 obj.method() |
工厂方法、操作类数据 |
| 静态方法 | 无 | Class.method() |
工具函数,与类相关但不需要类/实例 |
1 | class Date: |
Q4:为什么实现了 __eq__ 后对象就不可哈希了?
答案:Python 默认认为相等的对象应该有相同的哈希值。如果只实现 __eq__ 而不实现 __hash__,Python 会将 __hash__ 设为 None,表示对象不可哈希。
1 | class Point: |
Q5:@property 的作用是什么?
答案:@property 将方法变成属性访问,用于:
- 实现只读属性
- 添加访问验证
- 计算属性(延迟计算)
1 | class Circle: |
Q6:什么是描述符?
答案:描述符是实现了 __get__、__set__、__delete__ 中任意一个的类,用于控制属性访问。
1 | class Validator: |
Q7:@dataclass 的优势是什么?
答案:@dataclass 自动生成 __init__、__repr__、__eq__ 等方法,减少样板代码。
1 | from dataclasses import dataclass, field |
Q8:元类的作用和使用场景?
答案:元类是”类的类”,控制类的创建过程。常见用途:
- 自动注册子类(工厂模式)
- 添加类属性/方法
- 接口检查
1 | # 简化方案:__init_subclass__(Python 3.6+) |
实战项目:简易 ORM 框架
通过实现一个迷你 ORM(对象关系映射),综合运用 OOP 核心知识。
项目目标
创建一个支持以下功能的 ORM:
- 类到表的映射
- 字段类型验证
- 简单查询 API
- 关联关系
完整代码
1 | #!/usr/bin/env python3 |
运行示例
1 | ✓ 表创建成功 |
知识点覆盖
| 功能 | 涉及知识点 |
|---|---|
| 字段类型 | 描述符(__get__、__set__) |
| 模型元信息 | 元类(__new__)、类属性收集 |
| 数据库操作 | 魔术方法(__init__、__repr__) |
| 类型验证 | 描述符中的验证逻辑 |
| 继承体系 | 基类设计、子类继承 |
进阶练习
- 添加
update()和delete()方法 - 支持外键关联(一对多、多对多)
- 添加查询构建器(链式调用)
- 支持迁移(自动检测模型变化)
- 添加事务支持
附录:第三阶段自检清单
在继续学习第四阶段之前,请确保你能:
- 解释
__new__和__init__的区别,以及各自的调用时机 - 区分实例方法、类方法和静态方法,并说出各自的使用场景
- 用
@property实现带验证的属性访问 - 解释 MRO 和 C3 线性化,画出多继承的 MRO 顺序
- 说明
super()在多继承中的行为 - 解释鸭子类型和抽象基类的区别,以及各自的使用场景
- 实现
__str__、__repr__、__len__、__getitem__、__iter__等常用魔术方法 - 用
__enter__和__exit__实现自定义上下文管理器 - 理解元类的作用,并能用
__init_subclass__替代常见元类场景 - 用
dataclass替代手写__init__和__repr__ - 手写单例模式、工厂模式和策略模式
- 解释
__hash__和__eq__的关系,以及为什么实现__eq__后对象不可哈希 - 使用
__slots__优化内存,并说明其限制 - 使用
Enum替代魔术数字 - 用描述符实现自定义验证属性
工程师寄语:Python 的 OOP 是”实用主义”的——它给你足够的工具,但不过度约束。理解魔术方法和协议比记住所有设计模式更重要。当你发现自己在重复写样板代码时,停下来想一想:是不是可以用
dataclass、描述符或元类来简化?
