文章
合集Python 核心与面向对象第 3 / 5 篇

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 线性化算法保证每个类只出现一次。

三条核心规则:

  1. 子类优先:D 总是第一个被查
  2. 从左到右:class D(B, C) → B 在 C 前
  3. 每个类只出现一次: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
1MyService{dsn: "postgres://...", log_level: "DEBUG"}name(直接赋值给实例){dsn: "...", log_level: "DEBUG"}
2LoggerMixin{dsn: "...", log_level: "DEBUG"}log_level="DEBUG"{dsn: "..."}
3DBConnector{dsn: "..."}dsn="postgres://..."{}(空包)
4object{}什么都不做结束

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