Python 面向对象进阶
抽象类
鸭子类型很灵活,但有隐患:子类可能忘记实现必要方法,错误只在运行时调用该方法时才暴露。
抽象类(Abstract Base Class, ABC) 通过在实例化时强制检查,将错误提前发现。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
def describe(self): # 非抽象方法:子类可直接继承
print(f"面积: {self.area():.2f},周长: {self.perimeter():.2f}")
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
import math
return math.pi * self.r ** 2
def perimeter(self):
import math
return 2 * math.pi * self.r
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
def perimeter(self):
return 2 * (self.w + self.h)
# Shape() # ❌ TypeError: Can't instantiate abstract class Shape
c = Circle(5)
c.describe() # 面积: 78.54,周长: 31.42
抽象属性:
class Config(ABC):
@property
@abstractmethod
def host(self) -> str: # 子类必须实现 host 属性
pass
抽象类 = 带强制规则的父类。它定义"必须有什么方法",而不关心"怎么实现"。结合多态使用,既灵活又安全。
@abstractmethod背后的元类机制见 python-metaprogramming → 「元类」
鸭子类型
"如果它走路像鸭子、叫声像鸭子,那它就是鸭子。"
Python 不在乎对象的类型,只在乎对象有没有需要的方法。这意味着无需继承关系也能实现多态:
class Dog:
def speak(self): print("汪汪")
class Robot:
def speak(self): print("哔哔")
class Duck:
def speak(self): print("嘎嘎")
def make_noise(thing): # 不检查类型,只要有 speak 方法就行
thing.speak()
for obj in [Dog(), Robot(), Duck()]:
make_noise(obj)
鸭子类型 vs 抽象类:
| 方式 | 特点 | 适用场景 |
|---|---|---|
| 鸭子类型 | 完全动态,不检查类型或继承 | 小项目、原型、灵活集成 |
| 抽象类 | 编写时有约束,错误早暴露 | 团队协作、大型项目、稳定接口 |
| 概念 | 含义 |
|---|---|
| 强类型 | 类型不能随意混用("5" - 2 报错) |
| 弱类型 | 隐式乱转换(JS:"5" - 2 = 3) |
| 动态类型 | 运行时确定类型(变量可以指向任意类型) |
| 静态类型 | 编译期确定类型(Java、C++ 的做法) |
鸭子类型的灵活性来自动态类型,而不是弱类型。
组合优于继承
继承(is-a) 和 组合(has-a) 都能实现代码复用,但含义和耦合度截然不同。
继承的陷阱
class Bird:
def fly(self): print("飞翔")
def eat(self): print("进食")
class Penguin(Bird): # ❌ 企鹅不会飞,却被迫继承了 fly
pass
继承的问题:
- 语义错误:Penguin is-a Bird,但 fly 方法是错的
- 高耦合:修改父类的
fly会影响所有子类 - 强迫继承不需要的方法:子类背负了不该有的行为
组合解决方案
from abc import ABC, abstractmethod
class FlyBehavior(ABC):
@abstractmethod
def fly(self): pass
class CanFly(FlyBehavior):
def fly(self): print("展翅高飞")
class CannotFly(FlyBehavior):
def fly(self): print("我不会飞")
class Bird:
def __init__(self, name, fly_behavior: FlyBehavior):
self.name = name
self.fly_behavior = fly_behavior # 组合:持有行为对象
def fly(self):
self.fly_behavior.fly()
sparrow = Bird("麻雀", CanFly())
penguin = Bird("企鹅", CannotFly())
sparrow.fly() # 展翅高飞
penguin.fly() # 我不会飞
# 运行时动态替换行为
sparrow.fly_behavior = CannotFly()
sparrow.fly() # 我不会飞(翅膀受伤了)
判断口诀:
- "A 是 B" → 用继承(
Dog是Animal) - "A 有 B" / "A 使用 B" → 用组合(
Car有Engine,User使用Database)
组合将"行为"拆成独立模块,可以动态替换,耦合度低。这正是策略模式的核心思想。
多重继承与 Mixin
Python 支持一个类同时继承多个父类,最实用的方式是 Mixin 模式。
Mixin 是什么
Mixin 是一种只提供特定功能、不代表完整对象概念的类,通过多重继承"混入"目标类:
- 命名约定:以
Mixin结尾 - 职责单一:每个 Mixin 只做一件事
- 不应单独实例化
- 通常不持有状态(或持有极少状态)
- 继承时放在基类前面:
class MyClass(Mixin1, Mixin2, BaseClass)
import json
class JsonMixin:
"""提供 JSON 序列化能力"""
def to_json(self):
return json.dumps(self.__dict__, ensure_ascii=False)
@classmethod
def from_json(cls, json_str):
data = json.loads(json_str)
obj = cls.__new__(cls)
obj.__dict__.update(data)
return obj
class LogMixin:
"""提供日志打印能力"""
def log(self, msg):
print(f"[{self.__class__.__name__}] {msg}")
class ValidateMixin:
"""提供字段校验能力"""
def validate(self):
for key, value in self.__dict__.items():
if value is None:
raise ValueError(f"字段 {key} 不能为空")
class User(JsonMixin, LogMixin, ValidateMixin):
def __init__(self, name, email):
self.name = name
self.email = email
u = User("Alice", "alice@example.com")
print(u.to_json()) # {"name": "Alice", "email": "alice@example.com"}
u.log("登录成功") # [User] 登录成功
u.validate() # 通过校验
Mixin vs 普通多重继承
| 对比点 | Mixin | 普通多重继承 |
|---|---|---|
| 意图 | 混入功能 | 组合多个概念 |
| 是否独立使用 | 不应该 | 通常可以 |
| 状态 | 尽量无状态 | 通常有自己的状态 |
| 命名 | XxxMixin | 普通类名 |
MRO(方法解析顺序)
多重继承时,当多个父类有同名方法,Python 按 MRO(Method Resolution Order) 决定调用哪一个。
查看 MRO
class A:
def foo(self): print("A")
class B(A):
pass
class C(A):
def foo(self): print("C")
class D(B, C):
pass
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
D().foo() # C(按 MRO 找,B 没有 foo,C 有,用 C 的)
菱形继承与 C3 线性化
A
/ \
B C
\ /
D
没有 MRO,A 会被调用两次(分别通过 B 和 C)。Python 用 C3 线性化算法保证每个类只出现一次。
三条核心规则:
- 子类优先:D 总是第一个被查
- 从左到右:
class D(B, C)→ B 在 C 前 - 每个类只出现一次:A 只被查一次
super()
super() 不是"调用父类",而是沿 MRO 链调用下一个类:
class A:
def foo(self):
print("A")
class B(A):
def foo(self):
print("B")
super().foo() # MRO 中 B 的下一个是 C
class C(A):
def foo(self):
print("C")
super().foo() # MRO 中 C 的下一个是 A
class D(B, C):
def foo(self):
print("D")
super().foo() # MRO 中 D 的下一个是 B
D().foo()
# D → B → C → A
每个类只需调用 super(),整条 MRO 链就被串联起来,A 只执行一次。这种模式叫协作式多重继承(Cooperative Multiple Inheritance)。
super() 在 __init__ 中的用法:
class Base:
def __init__(self):
print("Base.__init__")
class Mixin:
def __init__(self):
print("Mixin.__init__")
super().__init__() # Mixin 也要调用 super,才能串联整条链
class MyClass(Mixin, Base):
def __init__(self):
print("MyClass.__init__")
super().__init__()
MyClass()
# MyClass.__init__
# Mixin.__init__
# Base.__init__
设计原则(SOLID)
SOLID 是五条面向对象设计原则,遵守它们能让代码更易维护和扩展。
S — 单一职责原则(Single Responsibility)
一个类只做一件事,只有一个改变它的理由。
# ❌ 违反:UserManager 同时负责数据存储、发邮件、生成报告
class UserManager:
def save(self, user): ...
def send_welcome_email(self, user): ...
def generate_report(self): ...
# ✅ 遵守:每个类只有一个职责
class UserRepository:
def save(self, user): ...
class EmailService:
def send_welcome(self, user): ...
class ReportService:
def generate(self): ...
O — 开闭原则(Open/Closed)
对扩展开放,对修改关闭。新增功能通过添加新类实现,而不是修改已有代码。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: pass
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
def total_area(shapes: list[Shape]) -> float:
return sum(s.area() for s in shapes)
# 新增 Triangle 完全不需要修改 total_area
class Triangle(Shape):
def __init__(self, b, h): self.b, self.h = b, h
def area(self): return 0.5 * self.b * self.h
L — 里氏替换原则(Liskov Substitution)
子类必须能替换父类使用,且不改变程序的正确性。
# ❌ 违反 LSP 的经典案例
class Rectangle:
def __init__(self, w, h): self.w, self.h = w, h
def set_width(self, w): self.w = w
def set_height(self, h): self.h = h
def area(self): return self.w * self.h
class Square(Rectangle):
def set_width(self, w):
self.w = self.h = w # 破坏了 Rectangle "宽高独立" 的语义
def check_area(rect: Rectangle):
rect.set_width(4)
rect.set_height(5)
assert rect.area() == 20 # ❌ Square 会失败:area = 25
# ✅ 修正:正方形和长方形是并列关系,不该继承
class Shape(ABC):
@abstractmethod
def area(self) -> float: pass
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
class Square(Shape):
def __init__(self, side): self.side = side
def area(self): return self.side ** 2
I — 接口隔离原则(Interface Segregation)
不应强迫客户端依赖它不需要的方法。将宽泛接口拆分为多个专用接口。
# ❌ 违反:一个大接口,企鹅被迫实现 fly
class Animal(ABC):
@abstractmethod
def eat(self): pass
@abstractmethod
def fly(self): pass # 企鹅不会飞,但必须实现
@abstractmethod
def swim(self): pass # 鹰不会游泳,但必须实现
# ✅ 遵守:拆分为多个专用接口
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class Flyable(ABC):
@abstractmethod
def fly(self): pass
class Swimmable(ABC):
@abstractmethod
def swim(self): pass
class Eagle(Eatable, Flyable): # 鹰:只实现它需要的
def eat(self): print("鹰吃东西")
def fly(self): print("鹰在飞")
class Penguin(Eatable, Swimmable): # 企鹅:不被强迫实现 fly
def eat(self): print("企鹅吃东西")
def swim(self): print("企鹅在游泳")
D — 依赖倒置原则(Dependency Inversion)
高层模块不应依赖低层模块,两者都应依赖抽象。
# ❌ 违反:高层 UserService 直接依赖低层 MySQLDatabase
class MySQLDatabase:
def save(self, data): print(f"MySQL 保存: {data}")
class UserService:
def __init__(self):
self.db = MySQLDatabase() # 写死了,换数据库就要改这里
# ✅ 遵守:依赖抽象,通过构造函数注入
class Database(ABC):
@abstractmethod
def save(self, data): pass
class MySQLDatabase(Database):
def save(self, data): print(f"MySQL 保存: {data}")
class RedisDatabase(Database):
def save(self, data): print(f"Redis 保存: {data}")
class UserService:
def __init__(self, db: Database): # 依赖抽象,不依赖具体
self.db = db
def create_user(self, user):
self.db.save(user)
# 切换数据库只需要换注入的对象
svc = UserService(MySQLDatabase())
svc = UserService(RedisDatabase())
协作式继承
PEP 推荐的协作式继承的代码结构,本质上是四条强制约定:
class DBConnector:
def __init__(self, dsn=None, **kwargs): # 约定1:自己的参数设默认值
self.dsn = dsn
print(f"[DB] 连接: {dsn}")
super().__init__(**kwargs) # 约定2:继续往后传
class LoggerMixin:
def __init__(self, log_level="INFO", **kwargs): # 约定1
self.log_level = log_level
print(f"[LOG] 级别: {log_level}")
super().__init__(**kwargs) # 约定2
class MyService(LoggerMixin, DBConnector): # 注意继承顺序
def __init__(self, name, dsn, log_level):
self.name = name
# 约定3:子类一次性打包所有参数
super().__init__(dsn=dsn, log_level=log_level)
执行 super().__init__(dsn=dsn, log_level=log_level) 时,参数被逐层消费:
| 步骤 | 当前执行类 | 收到的 kwargs | 消费了什么 | 传给下一个的 kwargs |
|---|---|---|---|---|
| 1 | MyService | {dsn: "postgres://...", log_level: "DEBUG"} | name(直接赋值给实例) | {dsn: "...", log_level: "DEBUG"} |
| 2 | LoggerMixin | {dsn: "...", log_level: "DEBUG"} | log_level="DEBUG" | {dsn: "..."} |
| 3 | DBConnector | {dsn: "..."} | dsn="postgres://..." | {}(空包) |
| 4 | object | {} | 什么都不做 | 结束 |
property
property 是 Python 内置的数据描述符,提供 getter/setter/deleter 语法,适合单个类内的属性访问控制。
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("温度低于绝对零度")
self._celsius = value
@property
def fahrenheit(self): # 只读计算属性,没有 setter
return self._celsius * 9/5 + 32
t = Temperature(100)
print(t.fahrenheit) # 212.0,像访问普通属性
t.celsius = -300 # ❌ ValueError
property vs 自定义描述符:
| 对比点 | property | 自定义描述符 |
|---|---|---|
| 复用性 | 只在当前类有效 | 可跨类复用 |
| 代码量 | 少 | 稍多,但只写一次 |
| 适用场景 | 单个类的属性控制 | 多个类共享同一套访问逻辑 |
property 的底层是描述符,完整原理见 python-metaprogramming → 「描述符」
slots
一、默认机制:dict 是个动态字典
每个 Python 实例都有一个 __dict__:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.__dict__) # {'x': 1, 'y': 2}
__dict__ 是哈希表(Hash Table):
- 优点:可以动态添加任意属性,如
p.z = 3。 - 缺点:内存开销大。一个空字典本身就要占约 56 字节,还要为每个键(字符串对象)、值指针、哈希值、扩容预留空间买单。
二、slots 是什么?
__slots__ 是一个类级别的声明,告诉解释器:
"这个类的实例只有这些属性,不要给它们配字典,改用固定位置的紧凑存储。"
class Point:
__slots__ = ('x', 'y') # 用tuple定义允许绑定的属性名称
def __init__(self, x, y):
self.x = x
self.y = y
底层变化:
- 解释器不再给实例创建
__dict__。 - 实例内部变成类似 C 结构体的固定布局:
x和y各占一个固定槽位(slot),通过偏移量直接访问,无需哈希查找。
三、对比示例(行为 + 内存)
from pympler import asizeof
class WithDict:
"""默认:有 __dict__"""
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
"""声明 __slots__"""
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# ========== 行为差异 ==========
print("=== 行为差异 ===")
d = WithDict(1, 2)
s = WithSlots(1, 2)
# 1. 动态添加属性
d.z = 3
print(f"WithDict 动态添加 d.z = {d.z}") # 成功
try:
s.z = 3
except AttributeError as e:
print(f"WithSlots 动态添加 s.z 失败: {e}")
# 'Point' object has no attribute 'z'
# 2. __dict__ 是否存在
print(f"WithDict 有 __dict__: {hasattr(d, '__dict__')}") # True
print(f"WithSlots 有 __dict__: {hasattr(s, '__dict__')}") # False
# 3. 实例大小
print(f"\n=== 内存对比 ===")
# 创建大量对象
dict_objs = [WithDict(i, i+1) for i in range(1_000_000)]
slots_objs = [WithSlots(i, i+1) for i in range(1_000_000)]
# 抽样测量(测全部会卡死,抽 1000 个估算)
import random
sample = 1000
dict_sample = random.sample(dict_objs, sample)
slots_sample = random.sample(slots_objs, sample)
dict_total = sum(asizeof.asizeof(obj) for obj in dict_sample) / sample * 1_000_000
slots_total = sum(asizeof.asizeof(obj) for obj in slots_sample) / sample * 1_000_000
print(f"百万 WithDict: {dict_total / 1024 / 1024:.1f} MB")
print(f"百万 WithSlots: {slots_total / 1024 / 1024:.1f} MB")
print(f"节省比例: {(1 - slots_total/dict_total)*100:.1f}%")
四、slots 的底层原理
默认实例属性存取走 字典哈希查找(O(1) 但内存重);__slots__ 走 数组偏移直接访问(O(1) 且内存轻)。
【有 __slots__ 的实例内存布局】
├─ 对象头(引用计数、类型指针)
├─ slot 0: x 的值
└─ slot 1: y 的值
【无 __slots__ 的实例内存布局】
├─ 对象头
└─ __dict__ 指针 ──→ 指向另一个哈希表对象
├─ 'x': 1
└─ 'y': 2
__slots__给实例预定固定大小的存储,只允许声明过的属性,不创建__dict__,省内存。代价是不能动态添加属性,且继承时要遵守拼接规则。
魔法方法
**魔法方法(Magic Methods)**是 Python 解释器在特定时机自动调用的方法,以双下划线 __ 开头结尾,也叫 Dunder Methods(Double UNDERscore)。
魔法方法不是给你直接调用的。你写
len(obj),解释器实际执行了obj.__len__()。
对象表示
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vector({self.x}, {self.y})" # 调试用,应可还原对象
def __str__(self):
return f"({self.x}, {self.y})" # 用户友好的显示
v = Vector(3, 4)
print(repr(v)) # Vector(3, 4)
print(str(v)) # (3, 4)
print(v) # (3, 4),print 调用 __str__
经验法则:
__repr__应尽量写成可以重新创建对象的字符串;__str__是给最终用户看的。只定义一个时优先定义__repr__。
比较操作与 __hash__
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return isinstance(other, Point) and self.x == other.x and self.y == other.y
def __hash__(self):
# 实现 __eq__ 后必须同时实现 __hash__
# 否则 Python 会将 __hash__ 置为 None,对象无法用于 set 和 dict
return hash((self.x, self.y)) # 用参与比较的字段组成元组来哈希
def __lt__(self, other): return (self.x, self.y) < (other.x, other.y)
def __le__(self, other): return self == other or self < other
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # True
print({p1, p2}) # {Point(1, 2)},set 中只有一个
print({p1: "A"}) # 可以作为 dict 的 key
关键规则:
a == b必须意味着hash(a) == hash(b)。Python 规定:定义了__eq__后,如果不同时定义__hash__,__hash__自动被设为None,对象将无法哈希。
数值运算
class Vector:
def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): return f"Vector({self.x}, {self.y})"
def __add__(self, other): return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other): return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar): return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): return self.__mul__(scalar) # 支持 3 * v
def __neg__(self): return Vector(-self.x, -self.y)
def __abs__(self): return (self.x**2 + self.y**2) ** 0.5
v1, v2 = Vector(1, 2), Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(3 * v1) # Vector(3, 6)
print(abs(v1)) # 2.23...
容器协议
class Stack:
def __init__(self): self._items = []
def push(self, item): self._items.append(item)
def pop(self): return self._items.pop()
def __len__(self): return len(self._items) # 触发:len(obj)
def __bool__(self): return len(self._items) > 0 # 非空为真
def __contains__(self, item): return item in self._items # 触发:item in obj
def __getitem__(self, idx): return self._items[idx] # 触发:obj[0]
s = Stack()
print(bool(s)) # False(空栈)
s.push(1); s.push(2)
print(bool(s)) # True
print(1 in s) # True
print(len(s)) # 2
for item in s: print(item)
__bool__未定义时,Python 回退到__len__:长度为 0 则为假。两者都未定义时,对象永远为真。
属性访问控制
class SafeProxy:
def __init__(self, target):
object.__setattr__(self, '_target', target) # 绕过自定义 __setattr__
def __getattr__(self, name):
# 只在普通查找失败时触发(属性不存在时)
return getattr(self._target, name)
def __setattr__(self, name, value):
# 每次赋值都触发(包括存在的属性)
print(f"设置 {name} = {value}")
object.__setattr__(self, name, value)
def __getattribute__(self, name):
# 每次属性访问都触发(包括存在的属性),很少需要重写
return object.__getattribute__(self, name)
__getattr__ vs __getattribute__:
| 方法 | 触发时机 | 使用频率 |
|---|---|---|
__getattr__ | 正常查找失败时(属性不存在) | 常用 |
__getattribute__ | 每次属性访问都触发(包括存在的属性) | 很少 |
可调用与上下文管理
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x): # 让实例像函数一样调用
return x * self.factor
double = Multiplier(2)
print(double(5)) # 10
print(callable(double)) # True
# ---
class ManagedFile:
def __init__(self, path, mode):
self.path, self.mode = path, mode
def __enter__(self):
self.file = open(self.path, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
return False # 不压制异常
with ManagedFile("test.txt", "w") as f:
f.write("hello")
总结
Python 不通过继承扩展能力,而是通过实现协议(即一组魔法方法)来扩展。
| 你想让对象支持... | 你需要实现... | 解释器在何时调用... |
|---|---|---|
| 初始化参数 | __init__ | 实例创建后 |
被 len() 测量 | __len__ | 执行 len(obj) 时 |
被 print() 显示 | __repr__ / __str__ | 执行 print(obj) 时 |
| 当属性用(描述符) | __get__ / __set__ | 执行 obj.attr 时 |
被 with 管理 | __enter__ / __exit__ | 进入/退出 with 块时 |
| 可迭代 | __iter__ / __next__ | for i in obj: 时 |
支持 + - == | __add__ / __sub__ / __eq__ | 执行运算符时 |
这就是 Python 的鸭子类型:不检查你是不是 list 的子类,只检查你有没有 __len__ 和 __getitem__。
__get__/__set__属于描述符协议,见 python-metaprogramming → 「描述符」
基础知识见 → python-oop-basics 高级元编程见 → python-metaprogramming