前言

这是一个关于 Python 面向对象编程的故事。故事的主角叫小王。

小王刚刚从培训班毕业,入职了一家小型软件公司。上班第一天,老板把他叫到办公室,拍着他的肩膀说:“小王啊,我们最近接了一个宠物店管理系统的项目,客户催得急。听说你学过 Python,这个项目就交给你了。”

小王回到工位,信心满满地打开电脑。他心想:一个宠物店管理系统而已,不就是写几个列表、几层循环吗?于是他二话不说,撸起袖子就开干。

七天后,系统终于跑起来了。小王松了一口气。然而,当他把系统交给客户试用时,问题接踵而至:客户说能不能加一个"给宠物洗澡"的功能,小王改了三天,把代码改得千疮百孔;客户说能不能把"狗"的分类再细分成"大型犬"和"小型犬",小王发现自己写了十几处 if animal == 'dog' 的判断,根本不知道从何改起;更可怕的是,系统时不时会莫名其妙地把宠物名字和价格搞混,小王连调试都无从下手。

小王崩溃了。他找到公司的老程序员李哥求助。李哥看了一遍他的代码,只说了一句话:“你用面向过程的方式写 Python 代码,就像用菜刀砍树——能砍,但累死人。”

李哥给小王倒了一杯水,语重心长地说:“面向对象编程,它不是一种语法,而是一种看世界的方式。今天我就用你这个宠物店项目,带你从头到尾走一遍。等你真正理解了它,你会发现自己写的代码就像活了一样,改起来不费劲,扩展起来也不头疼。”

小王点点头,拿出了笔记本。

这篇长文,就是李哥给小王讲课的完整内容。我们将跟随小王的视角,从类与对象的最基础概念开始,一步步走过封装、继承、多态、设计模式、SOLID 原则,最终抵达 Python 现代 OOP 特性的领地。全文以"宠物店"为主线故事,穿插了大量生活化比喻和完整可运行的代码示例。无论你是刚刚接触 OOP 的新手,还是想系统梳理知识的开发者,都欢迎坐下来,听李哥把这门课讲完。

本文阅读路线图

章节 核心问题 对应现实场景
一、类与对象 什么是类?什么是对象? 宠物档案卡与一只具体的狗
二、封装 为什么数据不能随便改? 银行账户的余额保护
三、继承与多态 怎样让代码复用又不失灵活? 动物家族的共同行为
四、设计模式 常见问题有没有标准解法? 数据库连接、工厂流水线、公众号订阅
五、SOLID 原则 好代码有没有评判标准? 宠物店系统的三次重构
六、现代 OOP 特性 Python 给出了哪些新工具? 数据类、类型注解、内存优化

准备好了吗?让我们正式开始。


一、类与对象:一切从"宠物档案卡"开始

1.1 基本概念

故事:宠物店的第一张档案卡

小王接手的宠物店管理系统中,最核心的需求是管理每一只宠物。最开始,小王用字典来记录一只宠物的信息:

dog_buddy = {
    'name': 'Buddy',
    'age': 3,
    'species': 'Canis familiaris',
    'breed': 'Golden Retriever'
}

这个字典看起来很清楚:Buddy 是一只 3 岁的金毛犬。但问题很快就来了。店里新来了第二只狗、第三只狗、甚至一只猫,小王不得不复制粘贴一堆字典:

dog_lucky = {'name': 'Lucky', 'age': 5, 'species': 'Canis familiaris', 'breed': 'Poodle'}
dog_max = {'name': 'Max', 'age': 2, 'species': 'Canis familiaris', 'breed': 'Husky'}
cat_whiskers = {'name': 'Whiskers', 'age': 1, 'species': 'Felis catus', 'breed': 'British Shorthair'}

然后他发现了一个严重的问题:某天他不小心把 dog_maxage 写成了字符串:

dog_max['age'] = '2'

系统里所有跟年龄相关的计算都崩了,而且他完全不知道是哪个数据出了问题。李哥看到这里,微笑着说了第一句重要的话:

如果系统里有很多"同类型的数据",与其用一堆孤立的字典,不如定义一个模板,让所有数据都按照这个模板来创建。这个模板,就是类(Class)。

什么是类?什么是对象?

在现实生活中,我们有无数个具体的"狗":Buddy、Lucky、Max……我们的大脑不会为每一只狗都重新建立一套认知,而是会抽象出一个概念——“狗”。这个概念包含了一组特征(有品种、有名字、有年龄、会叫)和一组行为(叫、跑、吃)。当我们看到一只具体的狗时,我们就说:它是"狗"这个概念的一个实例

Python 的面向对象编程,就是把这个自然思维过程搬进代码:

  • 类(Class):就是那个抽象的概念,或者说"模板"。它描述了某类事物共有的属性(数据)和方法(行为)。
  • 对象(Object):就是按照模板创造出来的具体实例。每一只具体的狗,都是一个对象。

用一个更直观的比喻:

  • 是一张"宠物档案卡"模板,上面规定:每只宠物必须有名字、年龄、物种这几个栏目。
  • 对象就是填好内容的一张张档案卡:Buddy 的卡、Lucky 的卡、Whiskers 的卡。

另一个经典比喻:是蛋糕模具,对象是用模具烤出来的一个个蛋糕。模具只有一个,但蛋糕可以有很多个,每个蛋糕上的奶油和水果各不相同。

第一个类

现在,我们把"狗"写成一个 Python 类:

class Dog:
    """狗的类,描述所有狗的共同特征和行为"""

    # 类变量:所有 Dog 对象共享的属性
    species = 'Canis familiaris'

    def __init__(self, name, age):
        # 实例变量:每个对象独有的数据
        self.name = name
        self.age = age

    # 实例方法:对象的行为
    def bark(self):
        return f'{self.name} says Woof!'

逐行拆解:

  1. class Dog:——用 class 关键字定义一个名为 Dog 的类。Python 约定类名使用大驼峰命名(每个单词首字母大写)。
  2. species = 'Canis familiaris'——这是一个类变量。它属于类本身,所有从这个类创建出来的对象共享同一份数据。为什么?因为所有的狗都属于同一个物种,这部分数据没必要在每个对象里各存一份。
  3. def __init__(self, name, age):——这是构造方法,在创建对象时自动被调用。它的任务是给每个对象初始化"独一无二的数据"。
  4. self.name = name——name 是一个实例变量,每个对象都有自己独立的 name 值。self 指代"当前正在被创建(或正在调用方法)的那个对象"。
  5. def bark(self):——这是一个实例方法,定义了对象的行为。所有实例方法的第一参数都必须是 self,这样方法才能知道"是谁在调用我"。
创建对象与实例化

创建对象的过程叫实例化,语法简洁到不可思议:

dog = Dog('Buddy', 3)
print(dog.name)        # Buddy
print(dog.age)         # 3
print(dog.species)     # Canis familiaris
print(dog.bark())      # Buddy says Woof!

当 Python 执行 dog = Dog('Buddy', 3) 时,背后发生了两件事:

  1. 创建出一个空白的新对象;
  2. 自动调用 Dog.__init__(新对象, 'Buddy', 3),把 'Buddy'3 分别存进这个新对象的 nameage 里。

所以 self 不需要你手动传,Python 会自己把它填上。

完整案例:类变量 vs 实例变量

为了彻底搞懂类变量和实例变量的区别,我们来看一个对比实验:

class Dog:
    species = 'Canis familiaris'  # 类变量:所有狗共享

    def __init__(self, name, age):
        self.name = name           # 实例变量:每只狗独有
        self.age = age             # 实例变量:每只狗独有


# 创建两只狗
buddy = Dog('Buddy', 3)
lucky = Dog('Lucky', 5)

# 实例变量互不影响
print(buddy.name)   # Buddy
print(lucky.name)   # Lucky

# 类变量被所有对象共享
print(buddy.species)  # Canis familiaris
print(lucky.species)  # Canis familiaris

# 修改类变量会影响所有对象
Dog.species = 'Canis lupus familiaris'
print(buddy.species)  # Canis lupus familiaris
print(lucky.species)  # Canis lupus familiaris

# 但是!如果通过实例去赋值,只会创建一个同名的实例变量
buddy.species = 'I am unique!'
print(buddy.species)  # I am unique!(实例变量遮蔽了类变量)
print(lucky.species)  # Canis lupus familiaris(其他对象不受影响)

这个实验揭示了一个重要细节:通过 类名.类变量 修改会影响到所有对象;而通过 实例.类变量 赋值,Python 会悄悄给这个实例创建一个同名的实例变量,遮蔽类变量,却不会影响其他实例。这是初学者很容易踩的坑,务必牢记。

实例方法、类方法、静态方法

类的"方法"有三种,它们各自有明确的使用场景。李哥画了一张表:

方法类型 第一参数 装饰器 什么时候用 能访问实例变量吗 能访问类变量吗
实例方法 self 处理单个对象的数据和行为
类方法 cls @classmethod 处理类级别的数据,或提供创建对象的替代方式
静态方法 无特殊参数 @staticmethod 与类相关但不需要访问类或实例的工具函数

回到宠物店场景。小王需要从数据库里读出这样一行字符串:"Buddy,3",然后创建狗对象。他可以用类方法来实现这种"替代构造器":

class Dog:
    species = 'Canis familiaris'

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        return f'{self.name} says Woof!'

    # 类方法:第一个参数是 cls,代表类本身
    @classmethod
    def from_string(cls, data_str):
        name, age = data_str.split(',')
        return cls(name, int(age))

    # 静态方法:既不依赖类,也不依赖实例
    @staticmethod
    def is_valid_age(age):
        return 0 < age < 30


# 正常方式创建
dog1 = Dog('Buddy', 3)

# 用类方法从字符串创建
dog2 = Dog.from_string('Lucky,5')

print(dog1.bark())   # Buddy says Woof!
print(dog2.bark())   # Lucky says Woof!
print(Dog.is_valid_age(10))  # True
print(Dog.is_valid_age(100))  # False

这里的关键区别:

  • from_string类方法,因为它需要调用 cls(...) 来创建对象。它的第一参数是 cls,代表当前类。这样写的好处是:如果以后有了 Cat 子类,Cat.from_string("Whiskers,2") 会自动创建出 Cat 对象,而不是死板地只能创建 Dog 对象。
  • is_valid_age静态方法,它只是"跟狗有关的一个工具函数",既不访问实例数据,也不需要创建对象。所以它既不需要 self,也不需要 cls
小结

是模板、图纸、模具;对象是按模板生产出来的具体产品。__init__ 负责在"生产"时初始化每个对象的独有数据,self 代表当前对象本身。类变量在所有对象间共享,实例变量各管各的。类方法带 cls,静态方法啥也不带,两者都是为了解决"这个方法到底属于谁、需要访问什么数据"这个问题。

1.2 魔术方法:让对象"活"起来

故事:对象开始"说人话"

宠物店系统上线后,小王想在日志里打印一只宠物的信息。他拿出刚学到的知识,写了这样一个类:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age


buddy = Dog('Buddy', 3)

# 小王想把 dog 打印出来看看
print(buddy)  # <__main__.Dog object at 0x000001C2B7F2B4A0>

屏幕上出现的是一串天书:<__main__.Dog object at 0x...>。小王心想:我就想看看这只狗叫什么名字、几岁了,至于给我打印出一串内存地址吗?

李哥说:“Python 里有一类方法,前后都有双下划线,比如 __init__。它们被称为魔术方法,或者叫特殊方法双下划线方法。Python 解释器会在特定时机自动调用它们。你刚才的输出不好看,是因为 print() 在背后调用了一个叫 __str__ 的魔术方法;默认的 __str__ 只会打印对象类型和内存地址。你要做的,就是重写这个魔术方法。”

常见魔术方法一览

在李哥的指导下,小王把 Dog 类升级了:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 让 print() 输出友好的信息(面向用户)
    def __str__(self):
        return f'{self.name} ({self.age} years old)'

    # 让交互式终端和列表输出更详细的信息(面向开发者)
    def __repr__(self):
        return f'Dog(name={self.name!r}, age={self.age!r})'

    # 比较两只狗是否"相同"
    def __eq__(self, other):
        if not isinstance(other, Dog):
            return NotImplemented
        return self.name == other.name and self.age == other.age

    # 让对象支持 len(),返回狗的"年龄"作为长度
    def __len__(self):
        return self.age

    # 让对象支持相加操作:两只狗的年龄相加
    def __add__(self, other):
        if not isinstance(other, Dog):
            return NotImplemented
        return self.age + other.age


buddy = Dog('Buddy', 3)
buddy_clone = Dog('Buddy', 3)
lucky = Dog('Lucky', 5)

print(buddy)            # Buddy (3 years old)  <- __str__
print(repr(buddy))      # Dog(name='Buddy', age=3)  <- __repr__
print(buddy == buddy_clone)  # True  <- __eq__
print(buddy == lucky)   # False
print(len(buddy))       # 3  <- __len__
print(buddy + lucky)    # 8  <- __add__

一瞬间,Dog 对象活了过来:它可以被友好地打印、可以做比较、可以跟 len() 配合、甚至可以用 + 号相加。这就是魔术方法的魔力。

用 Vector 类深入理解魔术方法

为了更系统地展示魔术方法,我们来看一个数学向量的例子。向量有两个分量 xy,支持加法和比较是再自然不过的事:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = 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 __eq__(self, other):
        return self.x == other.x and self.y == other.y

    # 使用 len() 时自动调用
    def __len__(self):
        # 向量的"长度"在这里定义为四舍五入后的模长
        return int((self.x**2 + self.y**2)**0.5)

    # 让对象可迭代:依次产出 x 和 y
    def __iter__(self):
        yield self.x
        yield self.y

    # 支持负数运算
    def __neg__(self):
        return Vector(-self.x, -self.y)

    # 支持减法
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)


v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1)          # Vector(3, 4)
print(v1 + v2)     # Vector(4, 6)
print(v1 - v2)     # Vector(2, 2)
print(v1 == Vector(3, 4))  # True
print(len(v1))     # 5
print(-v1)         # Vector(-3, -4)
print(list(v1))    # [3, 4]

你可能会想:这不就是普通的算数吗?我用函数 add(v1, v2) 也能做加法啊。没错,但关键在于表达力。当你写 v1 + v2 时,代码的意图一目了然;而 add(v1, v2) 还需要读者去查这个 add 函数到底干了什么。魔术方法的作用,就是让自定义类型融入 Python 语言本身,享受跟内建类型(如 intstrlist)完全相同的"待遇"。

魔术方法背后的调用机制

李哥特别强调了一个容易误解的点:

你写 v1 + v2,Python 其实做的是 v1.__add__(v2)。你写 len(v1),Python 做的是 v1.__len__()。你写 str(v1),Python 做的是 v1.__str__()。魔术方法永远不需要你手动调用,它们由 Python 在适当的语法场景下自动触发。

常用魔术方法速查表:

魔术方法 触发时机 用途
__init__(self, ...) 创建对象时 初始化实例变量
__str__(self) print()str() 面向用户的友好输出
__repr__(self) 交互式终端、repr()、容器内部展示 面向开发者的调试输出
__eq__(self, other) == 比较 判断两个对象是否相等
__lt__(self, other) <sorted() 自定义排序规则
__len__(self) len() 返回容器长度
__add__(self, other) + 加法运算
__sub__(self, other) - 减法运算
__getitem__(self, key) obj[key] 让对象像列表/字典一样索引
__setitem__(self, key, value) obj[key] = value 让对象支持索引赋值
__call__(self, ...) obj() 让对象可以像函数一样被调用
__enter__ / __exit__ with 语句 上下文管理
小结

魔术方法就像是给对象安装的"传感器":当 Python 的语言符号(+==len()print()……)触碰到对象时,对应的传感器被激活,从而执行你预先定义好的逻辑。用好魔术方法,你写的类就能像内建类型一样自然、流畅、充满 Python 味。


二、封装:给数据装上"安全门"

2.1 属性访问控制

故事:一个"可以随便改"的银行账户

宠物店项目暂时稳定了。小李(客户那边的负责人)又找上了小王:“我们宠物店要上线一个会员储值功能,宠物主人可以先充值,之后消费直接扣余额。”

小王脑子一转,这不简单吗?他写下了第一个版本:

class MemberAccount:
    def __init__(self, balance):
        self.balance = balance


account = MemberAccount(200)
# 顾客消费 30 元
account.balance -= 30
print(account.balance)  # 170

功能确实能用。但第二天,客户发现数据库里出现了好几条余额为负数的记录。原来,某个前端页面在计算折扣时出了问题,直接执行了:

account.balance = -500

一行代码就绕过了所有业务校验。李哥问小王:"你觉得问题出在哪里?"小王想了想:“balance 这个属性太’开放’了,任何人都能绕过业务逻辑直接改它。”

李哥点点头:“这就是**封装(Encapsulation)**要解决的问题。封装的核心理念是:把数据藏起来,只暴露必要的操作接口。就像银行不可能让储户自己打开金库门随便放钱取钱,储户必须通过柜台或者 ATM 操作。放到代码里,余额的修改必须通过存款、消费这些业务方法来完成,而不是直接赋值。”

三条访问级别

Python 没有像 Java、C++ 那样强制性的访问控制关键字(privateprotected),而是靠一套约定俗成的命名规则

写法 级别 访问规则 实际效果
self.balance 公开(public) 任何人都能读写 没有保护
self._balance 受保护(protected) 约定上不建议外部访问 外部仍可访问,但 IDE 会提示
self.__balance 私有(private) 外部"比较难"访问 Python 做名称改写,变成 _类名__balance

李哥提醒小王:“注意我的用词——Python 的私有是’比较难’访问,而不是’无法访问’。Python 哲学里有一句名言:我们都是成年人了(We are all consenting adults)。意思是:开发者被信任能做出正确的选择。下划线只是告诉别人’这个东西是内部实现细节,别碰’,真要碰,技术上也拦不住。”

正确的封装姿势

小王重写了会员账户类:

class MemberAccount:
    def __init__(self, balance=0):
        self._balance = balance          # 受保护属性:约定上不直接访问
        self.__transactions = []         # 私有属性:内部流水记录

    # property 让外部可以像读取属性一样读取余额
    @property
    def balance(self):
        return self._balance

    # 设置余额时自动做合法性校验
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError('Balance cannot be negative')
        self._balance = value

    # 存款
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError('Deposit amount must be positive')
        self._balance += amount
        self.__transactions.append(('deposit', amount))

    # 消费
    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError('Withdraw amount must be positive')
        if amount > self._balance:
            raise ValueError('Insufficient balance')
        self._balance -= amount
        self.__transactions.append(('withdraw', amount))

    # 只读访问交易流水
    def show_transactions(self):
        return list(self.__transactions)


account = MemberAccount(200)
account.deposit(50)
account.withdraw(30)
print(account.balance)  # 220

# 尝试非法操作
try:
    account.withdraw(9999)
except ValueError as e:
    print(e)  # Insufficient balance

# 直接赋值非法余额会被拦截
try:
    account.balance = -100
except ValueError as e:
    print(e)  # Balance cannot be negative

现在的 MemberAccount 类就像真正的银行系统:

  • 余额只能通过 deposit()withdraw() 两个"业务入口"修改;
  • @property 让外部代码依然可以写 account.balance 来读取,体验不变;
  • 尝试设置非法余额会立刻抛出异常;
  • 交易流水 __transactions 被彻底藏了起来,外部只能通过 show_transactions() 方法拿到副本。
@property 到底做了什么

@property 装饰器把"调用方法"变成了"访问属性"的体验。这在 Python 中是一个重要的设计思想:对外提供稳定的属性访问,对内保留灵活的实现逻辑

一个经典的好处是:假设你最初把 balance 设计成一个普通属性:

class Account:
    def __init__(self):
        self.balance = 0

后来产品需求变了:余额必须是"账户总额 - 冻结金额"的计算结果。如果一开始用的是普通属性,所有调用 account.balance 的地方都要改吗?不必——你把 balance 改成 @property,外部调用代码一行都不需要动:

class Account:
    def __init__(self, total, frozen):
        self._total = total
        self._frozen = frozen

    @property
    def balance(self):
        return self._total - self._frozen

这就是封装的另一层价值:在不破坏外部接口的前提下,自由地调整内部实现

2.2 封装在现实中的影子

封装不只存在于代码里。李哥给小王举了几个生活中的例子:

例子一:咖啡机。 你按下"美式"按钮,咖啡机内部要完成磨豆、加热、注水、冲泡等一系列步骤。但对用户来说,接口只有几个按钮。没有人会打开咖啡机的后盖去手动控制锅炉温度。咖啡机的"内部复杂性"被封装起来了。

例子二:体温计。 体温计测量体温,会直接给你一个数字。它不会告诉你热敏电阻的电压变化,也不需要你了解模数转换器的原理。

例子三:汽车。 你踩下油门,汽车前进。你不需要知道工程师怎么设计变速箱,也不关心喷油嘴怎么喷油。对驾驶员来说,方向盘、油门、刹车就是全部接口。

回到代码世界,封装的收益可以总结为三点:

  1. 安全性:数据不会被随意篡改。想象一下,任何一个模块都能直接把余额改成负数,系统还能稳定吗?
  2. 可维护性:内部实现可以自由调整,只要对外接口不变,调用方就无感知。
  3. 职责清晰:类的用户只需要关心"我能做什么"(方法),不需要理解"它内部怎么做"(数据)。
一个"反例"与"正例"的对决

小王还从李哥那里学到了一个判断封装好坏的实用技巧:看一个类暴露的公开接口是否足够少、足够清晰。下面这段代码是典型的"反例"——把所有东西都堆在外部:

# 反例:没有封装,业务规则散落各处
class Order:
    pass


order = Order()
order.items = []
order.total = 0
order.discount = 0
order.tax = 0

# 业务规则全部由外部代码承担
order.items.append(('dog_shampoo', 50))
order.total += 50
order.discount = order.total * 0.1
order.tax = (order.total - order.discount) * 0.06

再对比下面的"正例"——业务规则收进类内部:

# 正例:业务规则封装在类里
class Order:
    def __init__(self):
        self._items = []
        self._total = 0

    def add_item(self, name, price):
        if price < 0:
            raise ValueError('Price cannot be negative')
        self._items.append((name, price))
        self._total += price

    @property
    def total(self):
        return self._total

    @property
    def discount(self):
        if self._total > 300:
            return self._total * 0.1
        return 0

    @property
    def tax(self):
        return (self._total - self.discount) * 0.06

    @property
    def final_amount(self):
        return self._total - self.discount + self.tax


order = Order()
order.add_item('dog_shampoo', 50)
order.add_item('cat_food', 120)
order.add_item('dog_toy', 180)

print(order.total)        # 350
print(order.discount)     # 35.0(满 300 打九折)
print(order.tax)          # 18.9
print(order.final_amount)  # 333.9

高下立判。正例里,折扣规则、税费计算全部收进了类内部,外部调用者只需要 add_item() 加商品,然后读 final_amount 即可。以后就算折扣规则改为"满 500 减 80",也只需要修改 discount 这一个地方。

小结

封装的核心口号是:数据私有化,接口公开化。内部数据用下划线保护起来,外部只能通过经过设计的公开方法来操作。@property 让"方法"长得像"属性",兼顾了安全性和使用体验。当你在代码里发现某个数据被外部肆意修改、业务规则散落各处时,那就是需要封装的信号。


三、继承与多态:代码复用的利器

3.1 继承

故事:宠物店里的"动物家族"

宠物店管理系统的需求更新了:不仅要管理狗,还要管理猫、兔子、鹦鹉。小王看着自己写的 Dog 类,陷入沉思——难道要把 bark() 换成 meow(),再把 Dog 复制三份改成 CatRabbitParrot?每个类都重复写着 nameage__init__,代码越来越臃肿。

李哥说:“狗、猫、兔子、鹦鹉,它们虽然不同,但都是动物。它们共享一些共同的特征——都有名字、都有年龄、都能发出声音。与其在每个类里重复这些代码,不如提取出一个父类 Animal,把公共代码放进去,再让 DogCat继承它。”

这就是继承(Inheritance):子类从父类那里获得属性和方法,实现了代码复用。

class Animal:
    """所有动物的父类"""

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def eat(self):
        return f'{self.name} is eating.'

    def sleep(self):
        return f'{self.name} is sleeping.'

    def speak(self):
        # 父类不实现具体逻辑,要求子类必须重写
        raise NotImplementedError('Subclass must implement speak()')


class Dog(Animal):
    def speak(self):
        return f'{self.name} says Woof!'

    def fetch(self):
        return f'{self.name} runs to fetch the ball.'


class Cat(Animal):
    def speak(self):
        return f'{self.name} says Meow!'

    def chase_mouse(self):
        return f'{self.name} is chasing a mouse.'


class Rabbit(Animal):
    def speak(self):
        return f'{self.name} says no sound, just hops.'

    def hop(self):
        return f'{self.name} hops around happily.'


# 使用
buddy = Dog('Buddy', 3)
whiskers = Cat('Whiskers', 1)
fluffy = Rabbit('Fluffy', 2)

print(buddy.eat())      # Buddy is eating.(继承自父类)
print(whiskers.eat())   # Whiskers is eating.(继承自父类)
print(buddy.speak())    # Buddy says Woof!(子类自己的实现)
print(whiskers.speak()) # Whiskers says Meow!(子类自己的实现)
print(fluffy.hop())     # Fluffy hops around happily.(子类自己的方法)

在这个设计里:

  • Animal 定义了所有动物的公共部分nameageeat()sleep()
  • DogCatRabbit 都继承自 Animal,自动获得了这些属性和方法;
  • 每个子类重写speak(),因为每种动物的叫声不同;
  • 每个子类还可以增加自己独有的方法,如 fetch()chase_mouse()hop()
继承的层级设计:is-a 关系

使用继承前,一定要问自己一个问题:"子类 is a 父类"成立吗?

  • 动物?成立。 ✅
  • 动物?成立。 ✅
  • 汽车 发动机?不成立。 ❌
  • 员工 公司?不成立。 ❌

继承表达的是**“是一种”**关系,而不是"包含"关系。如果把"发动机"写成"汽车"的父类,那整个设计就错了。

深入理解 super():如何正确地调用父类方法

子类重写 __init__ 时,如果还想使用父类的初始化逻辑,就要用到 super()

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f'动物 "{name}" 已创建。')


class Dog(Animal):
    def __init__(self, name, age, breed):
        # 先调用父类的 __init__,完成 name 和 age 的初始化
        super().__init__(name, age)
        # 再处理子类自己的新属性
        self.breed = breed

    def speak(self):
        return f'{self.name} says Woof!'


buddy = Dog('Buddy', 3, 'Golden Retriever')
# 输出:动物 "Buddy" 已创建。

print(buddy.name)   # Buddy
print(buddy.age)    # 3
print(buddy.breed)  # Golden Retriever

super().__init__(name, age) 就是"调用父类的 __init__ 方法"。这个技巧非常关键:当父类有重要的初始化逻辑时,子类必须通过 super() 把它执行到位,否则父类中定义的属性就不会被设置。

3.2 多态

故事:一个循环遍历所有动物

宠物店系统里有一个"晨间点名"功能,每天早上要播放所有宠物的叫声。小王最初的想法是写一堆 if 判断:

animals = [dog, cat, rabbit]

for animal in animals:
    if isinstance(animal, Dog):
        print(animal.speak())
    elif isinstance(animal, Cat):
        print(animal.speak())
    elif isinstance(animal, Rabbit):
        print(animal.speak())

代码又丑又脆弱。如果以后增加一只鹦鹉,这个 if-elif 链还得继续加。

李哥说:“用多态。多态(Polymorphism) 的意思是:不同的对象对同一个消息做出不同的响应。你不需要知道每个对象具体是哪种动物,只需要统一调用 speak(),剩下的交给对象自己决定。”

animals = [Dog('Buddy', 3), Cat('Whiskers', 1), Rabbit('Fluffy', 2)]

for animal in animals:
    print(animal.speak())

# 输出:
# Buddy says Woof!
# Whiskers says Meow!
# Fluffy says no sound, just hops.

这就是多态的精髓:一个接口,多种形态speak() 这个消息只有一个,但不同的对象接收到后,会执行各自的版本。而且,以后添加新的动物类型(比如 Parrot),旧的循环代码一行都不用改,只需要新写一个继承自 Animal 的类即可。

多态的现实例子

李哥给小王的比喻是:你家电视、空调、音响,各自都有一个"电源开关"。你不需要知道电视内部是液晶面板还是 OLED,也不需要知道空调是变频还是定频,你只需要按"开关",设备自然就执行了正确的开机逻辑。这个"统一的开关接口"就是多态。

再比如,餐馆里顾客说"来一份招牌菜",中餐厨师会炒一盘宫保鸡丁,西餐厨师会煎一块牛扒,日料厨师会捏几个寿司。顾客不需要指定"按中餐做法做",指令一样,响应各不相同。

多态的两种实现方式

Python 的多态有两种常见形态:继承多态鸭子类型

继承多态就是上面演示的:子类重写父类方法,通过父类类型的引用统一调用。

鸭子类型是 Python 特有的。正如那句名言:“如果一只鸟走起来像鸭子、游起来像鸭子、叫起来像鸭子,那么它就可以被当作鸭子。”

class Duck:
    def speak(self):
        return 'Quack! Quack!'


class Person:
    def speak(self):
        return 'Hello, I am a person!'


class Bell:
    def speak(self):
        return 'Ding-dong!'


def make_sound(thing):
    # 我们根本不管 thing 是什么类型,
    # 只要它有 speak 方法,就能被调用
    return thing.speak()


print(make_sound(Duck()))     # Quack! Quack!
print(make_sound(Person()))   # Hello, I am a person!
print(make_sound(Bell()))     # Ding-dong!

在这个例子里,make_sound 函数完全没有要求传入对象必须继承哪个父类。它只认一件事:你有没有 speak() 方法。有,就能用。这种灵活性让 Python 代码可以非常简洁地实现多态,但也要求开发者更自律。

3.3 多重继承与 MRO

故事:一只会游泳又会飞的鸭子

宠物店里来了一位特殊顾客,牵着一只鸭子。店主为难了:这只鸭子既会游泳,又会飞,还会叫。用单继承怎么建模?让 Duck 只继承 Animal 显然无法体现它会游泳、会飞的特点。

Python 答案:多重继承。一个类可以同时继承多个父类:

class Swimmer:
    def swim(self):
        return 'Swimming in the pond.'


class Flyer:
    def fly(self):
        return 'Flying in the sky.'


class Duck(Swimmer, Flyer):
    pass


duck = Duck()
print(duck.swim())  # Swimming in the pond.
print(duck.fly())   # Flying in the sky.

Duck 同时继承了 SwimmerFlyer,所以它天然就会 swim()fly()。这在概念上很美好,但引发了一个严肃的问题:如果两个父类有同名方法,Python 该调用哪个?

MRO:方法解析顺序

假设我们有这样的类结构:

class A:
    def who_am_i(self):
        return 'I am A'


class B(A):
    def who_am_i(self):
        return 'I am B'


class C(A):
    def who_am_i(self):
        return 'I am C'


class D(B, C):
    pass


d = D()
print(d.who_am_i())  # I am B

D 继承了 BC,而 BC 都重写了 who_am_i()。Python 到底调用哪个?答案是 B 的版本。为什么?因为 MRO(Method Resolution Order,方法解析顺序) 决定了方法查找的顺序。

你可以用 __mro__ 属性查看任何类的完整查找顺序:

print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

解读:当调用 d.who_am_i() 时,Python 按照 D → B → C → A → object 的顺序依次查找,命中第一个定义了 who_am_i 的类就停。B 在最前面,所以 B.who_am_i 被调用。

MRO 背后是一个叫 C3 线性化算法 的复杂机制,对于日常开发,你只需要记住两条直觉规则:

  1. 子类优先于父类:查找顺序中,子类永远排在父类前面。
  2. 从左到右:多重继承时,写在括号前面的父类优先级更高。class D(B, C) 中,B 优先于 C
菱形继承:多重继承最典型的问题

在上面的例子里,BC 都继承自 A,这就形成了一颗"菱形"。如果 BC 都没有定义 who_am_i,而 A 定义了,调用 d.who_am_i() 时就会一路走到 A。那么问题来了:A 的方法会不会被调用两次?

不会。Python 的 MRO 保证了每个类在查找序列中只出现一次。这就是 C3 线性化的功劳。换句话说,D 的 MRO 是 D → B → C → A → objectA 只出现一次,方法不会被重复执行。

class A:
    def who_am_i(self):
        return 'I am A'


class B(A):
    pass


class C(A):
    pass


class D(B, C):
    pass


d = D()
print(d.who_am_i())  # I am A
print(D.__mro__)
# (<class '__main__.D'>, B, C, A, object)

再来看一个更贴近业务的例子:

class Swimmer:
    def __init__(self):
        self.can_swim = True
        super().__init__()


class Flyer:
    def __init__(self):
        self.can_fly = True
        super().__init__()


class Duck(Swimmer, Flyer):
    def __init__(self):
        super().__init__()
        self.name = 'Duck'


duck = Duck()
print(duck.can_swim)  # True
print(duck.can_fly)   # True
print(duck.name)      # Duck

这个例子非常神奇:Swimmer.__init__Flyer.__init__ 都被执行了!靠的就是 super() 配合 MRO 的协作式多继承。Duck.__init__ 里的 super() 顺着 MRO(Duck → Swimmer → Flyer → object)逐级调用,所以两个父类的初始化都被触发了。

多重继承的使用建议

多重继承威力强大,但也容易造成代码难以理解。李哥的建议有三条:

  1. 能不用就不用。绝大多数场景,单继承加组合就足够了。
  2. 如果必须用,优先继承 “Mix-in” 风格的轻量类。所谓 Mix-in,就是那些只提供一到两个方法的"能力类",比如 SwimmerFlyer,它们没有复杂的状态和层级。
  3. 避免菱形继承中的状态冲突。如果多个父类都定义了同名属性,调试起来会非常痛苦。
小结

继承表达"是一种"关系,让子类共享父类的代码;super() 负责正确调用父类的逻辑。多态是"一个接口、多种形态",让代码在运行时才决定具体执行哪个版本。多重继承是 Python 的强大武器,用 MRO 保证查找顺序可预测;但请克制使用,优先考虑组合。


四、设计模式:常见问题的标准解法

代码写得多了,你就会发现:很多问题虽然业务场景不同,但本质结构是一样的。前辈工程师们总结出了这些反复出现的问题的标准解法,这就是设计模式(Design Patterns)。它就像武术中的套路:经过千锤百炼,能让你在特定情境下快速出拳。

4.1 单例模式

故事:系统里有几个"数据库连接"?

宠物店系统要把数据存到数据库。每个数据库连接都要占用系统资源,如果每个请求都新建一个连接,系统很快就会崩溃。理想的方案是:整个程序运行期间,数据库连接池只有一个实例,所有人都共享它

这就是单例模式(Singleton Pattern):确保一个类只有一个实例,并且提供一个全局访问点。

class Singleton:
    _instance = None

    def __new__(cls):
        # 如果还没有实例,就创建一个
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        # 否则直接返回已有实例
        return cls._instance


s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # True
深入理解 newinit 的分工

你可能会有疑问:“平时创建对象不都是靠 __init__ 吗?这里怎么出现了一个 __new__?”

李哥画了一张图来解释:

__new__ 负责"造出对象",__init__ 负责"给对象填数据"。 __new____init__ 之前被调用,它的任务是创建并返回实例对象本身。只有 __new__ 有能力控制"是否真的创建一个新对象"。

在单例模式里,__new__ 被拦截了:第一次调用时,_instanceNone,于是创建新对象;第二次调用时,_instance 已经有值了,直接返回这个旧对象。结果就是无论你调用多少次 Singleton(),得到的永远是同一个对象。

一个更实用的单例:配置管理器
class ConfigManager:
    _instance = None
    _initialized = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        # 关键:确保初始化只执行一次
        if not self._initialized:
            self.config = {}
            self._initialized = True

    def set(self, key, value):
        self.config[key] = value

    def get(self, key):
        return self.config.get(key)


# 任何地方拿到配置管理器,都是同一个实例
cm1 = ConfigManager()
cm1.set('database_url', 'mysql://localhost:3306')

cm2 = ConfigManager()
print(cm2.get('database_url'))  # mysql://localhost:3306

print(cm1 is cm2)  # True

这里的 _initialized 标志位很关键:虽然 __new__ 保证了实例唯一,但 __init__ 每次创建时依然会被调用。如果不加防重复初始化标志,cm2 = ConfigManager() 时就会把配置清空。

何时使用单例模式

适用的场景:数据库连接池、配置管理、日志记录器、线程池、缓存等"整个程序只需要一个"的资源。

需要警惕的场景:不要为了用而用。很多人为了追求"高端",到处套单例模式,结果导致全局状态满天飞,程序难以测试和维护。判断标准很简单:这个类如果创建多个实例,会不会产生逻辑错误或资源浪费? 会,就考虑单例;不会,就不要用。

4.2 工厂模式

故事:宠物店的"流水线"上堆满了 if-else

随着业务发展,宠物店引入了更多类型的宠物:狗、猫、兔子、鹦鹉、仓鼠、乌龟……小王发现,创建宠物的代码里出现了大量重复的 if-elif-else

def create_pet(pet_type, name, age):
    if pet_type == 'dog':
        return Dog(name, age)
    elif pet_type == 'cat':
        return Cat(name, age)
    elif pet_type == 'rabbit':
        return Rabbit(name, age)
    elif pet_type == 'parrot':
        return Parrot(name, age)
    elif pet_type == 'hamster':
        return Hamster(name, age)
    elif pet_type == 'turtle':
        return Turtle(name, age)
    else:
        raise ValueError(f'Unknown pet type: {pet_type}')

这段代码不仅放在一个函数里,还散落在系统各处。每次新增一种宠物,小王都要满世界找这些判断分支。

李哥说:“把创建对象的逻辑集中到一个’工厂’里,调用方不需要知道具体的创建细节。这就是工厂模式(Factory Pattern)。”

class PetFactory:
    @staticmethod
    def create(pet_type, name, age):
        if pet_type == 'dog':
            return Dog(name, age)
        elif pet_type == 'cat':
            return Cat(name, age)
        elif pet_type == 'rabbit':
            return Rabbit(name, age)
        elif pet_type == 'parrot':
            return Parrot(name, age)
        else:
            raise ValueError(f'Unknown pet type: {pet_type}')


# 调用方只需要跟工厂打交道
pet = PetFactory.create('dog', 'Buddy', 3)
print(type(pet).__name__)  # Dog

工厂模式的收益是显而易见的:新增宠物类型时,只需要修改 PetFactory.create 这一个方法;所有调用方不需要做任何改动。

工厂模式的变体:注册式工厂 + 反射

上面的简单工厂还是需要 if-elif。Python 提供了更优雅的字典映射方式:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        return 'Woof!'


class Cat:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        return 'Meow!'


class PetFactory:
    # 注册表:类型名 -> 类
    _registry = {}

    @classmethod
    def register(cls, pet_type):
        """类装饰器:注册宠物类型"""
        def decorator(pet_class):
            cls._registry[pet_type] = pet_class
            return pet_class
        return decorator

    @classmethod
    def create(cls, pet_type, name, age):
        pet_class = cls._registry.get(pet_type)
        if pet_class is None:
            raise ValueError(f'Unknown pet type: {pet_type}')
        return pet_class(name, age)


@PetFactory.register('dog')
class Dog:
    pass


@PetFactory.register('cat')
class Cat:
    pass


# 创建宠物
pet = PetFactory.create('cat', 'Whiskers', 1)
print(type(pet).__name__)  # Cat

这种"注册式工厂"极其灵活:以后添加新宠物,只需要在类定义上加上 @PetFactory.register('新类型'),工厂就会自动认识它,完全不需要修改工厂内部的任何代码。

工厂模式在现实中的影子

现实生活中,工厂模式的影子无处不在:奶茶店的点单系统就是工厂。顾客说"一杯珍珠奶茶",店员(工厂)根据订单制作出对应的饮品,顾客不需要了解珍珠是怎么煮的、奶茶是怎么泡的。餐厅厨房、汽车总装线、蛋糕店……都是工厂模式的原型。

4.3 观察者模式

故事:宠物店发优惠通知

宠物店运营经理提了一个需求:每当有新宠物到店,系统要自动通知所有订阅了"新宠提醒"的会员(短信、邮件、APP 推送一个都不能少)。

小王最初用了最暴力的方法:

def new_pet_arrived(pet_info):
    send_sms(pet_info)
    send_email(pet_info)
    send_app_push(pet_info)

但需求持续变化:有些会员只订阅短信,有些只要 APP 推送;以后可能还要接入微信通知、站内信……小王改来改去,代码越来越混乱。

李哥说:“这是典型的观察者模式(Observer Pattern)。核心思想是:一个被观察者(Subject)维护一个观察者列表,当它的状态改变时,自动通知列表中的所有观察者。观察者可以随时订阅(attach)或退订(detach),被观察者完全不需要知道谁在关注它。”

class Subject:
    """被观察者:宠物店"""

    def __init__(self):
        self._observers = []  # 订阅者列表

    def attach(self, observer):
        """新增订阅者"""
        self._observers.append(observer)

    def detach(self, observer):
        """移除订阅者"""
        self._observers.remove(observer)

    def notify(self, message):
        """通知所有订阅者"""
        for observer in self._observers:
            observer.update(message)


class SmsObserver:
    def update(self, message):
        print(f'[短信] {message}')


class EmailObserver:
    def update(self, message):
        print(f'[邮件] {message}')


class AppPushObserver:
    def update(self, message):
        print(f'[APP推送] {message}')


# 使用
store = Subject()
store.attach(SmsObserver())
store.attach(EmailObserver())
store.attach(AppPushObserver())

# 新宠到店,一次通知三个渠道
store.notify('新到店:3岁金毛 Buddy,欢迎选购!')

# 输出:
# [短信] 新到店:3岁金毛 Buddy,欢迎选购!
# [邮件] 新到店:3岁金毛 Buddy,欢迎选购!
# [APP推送] 新到店:3岁金毛 Buddy,欢迎选购!

# 某个会员退订了短信
sms_observer = SmsObserver()
store.attach(sms_observer)
store.detach(sms_observer)

这个设计优雅在:添加新的通知渠道不需要改动 Subject。你只需要写一个新的观察者类,然后 attach 一下就行了。这完美符合了后面要讲的 SOLID 原则中的"开闭原则"——对扩展开放,对修改关闭。

现实中的观察者模式

微信公众号的"订阅-推送"机制就是最典型的观察者模式:你(观察者)订阅了一个公众号(被观察者),公众号发新文章时,你和其他订阅者都会收到推送;你取关后就不再收到。股票行情软件、RSS 订阅、MQTT 消息队列……背后都有观察者模式的影子。

小结

单例模式解决"只有一个实例"的问题,工厂模式解决"创建对象的复杂性"问题,观察者模式解决"一对多通知"的问题。它们不是教条,而是前人总结的高效套路。学习设计模式最大的收获不是记住代码,而是培养识别问题本质的能力:看到"全局唯一资源"就想到单例,看到"分散创建逻辑"就想到工厂,看到"状态变化通知多个依赖者"就想到观察者。


五、SOLID 原则:面向对象的设计哲学

系统稳定运行了一个阶段。但随着功能不断增加,小王发现自己改一个需求,常常要连带修改多处代码;写一个测试,要花大量的精力去 mock 各种依赖。他问李哥:“到底什么样的面向对象设计才算’好’?有没有一套客观的标准?”

李哥说:“有。SOLID 原则就是面向对象设计的五条黄金准则。SOLID 是五个单词首字母的缩写:S 单一职责、O 开闭原则、L 里氏替换、I 接口隔离、D 依赖倒置。它们不是死命令,而是帮助我们识别’代码坏味道’的指南针。”

5.1 S — 单一职责原则

一句话:一个类只应该有一个引起它变化的原因。

翻译成人话:一个类只负责一件事。如果一个类什么都做,那它就会因为各种原因被反复修改,代码耦合度急剧上升。

反例:宠物店系统里的"上帝类"——它既处理宠物信息,又处理支付逻辑,还处理数据库连接:

class PetStore:
    def add_pet(self, pet):
        # 业务逻辑:添加宠物
        ...

    def checkout(self, amount):
        # 支付逻辑
        ...

    def query_pet_by_name(self, name):
        # 数据库查询
        ...

    def send_email(self, to, content):
        # 邮件发送
        ...

这个类有四个职责,任何一个需求变化都要动它,如同一个穿着四种制服的人,什么都要掺和一脚。

正例

class PetRepository:
    """只负责宠物数据的持久化"""
    def add(self, pet):
        ...

    def query_by_name(self, name):
        ...


class PaymentService:
    """只负责支付"""
    def checkout(self, amount):
        ...


class EmailService:
    """只负责邮件发送"""
    def send(self, to, content):
        ...

修改支付逻辑时,不会波及宠物数据的处理;修改邮件模板时,不影响支付。各有各的"一亩三分地"。

5.2 O — 开闭原则

一句话:软件实体应该对扩展开放,对修改关闭

意思是:当需求变化时,你应该通过新增代码来扩展功能,而不是修改已有代码。修改已有代码的风险在于,你可能会破坏原本正常工作的功能。

反例:宠物种类每新增一种,都要修改 speak 逻辑:

def make_sound(pet_type):
    if pet_type == 'dog':
        return 'Woof!'
    elif pet_type == 'cat':
        return 'Meow!'
    # 每次新增宠物都要进来修改这个函数

正例:通过多态实现——新增宠物类型只需新增子类,不必修改已有函数:

class Dog(Animal):
    def speak(self):
        return 'Woof!'


class Cat(Animal):
    def speak(self):
        return 'Meow!'


# 新增一种宠物,完全不需要改上面的代码
class Parrot(Animal):
    def speak(self):
        return 'Squawk!'

make_sound 函数不需要任何改动,因为新增的 Parrot 类自动融入了多态体系。这就是"对扩展开放,对修改关闭"。

5.3 L — 里氏替换原则

一句话:子类必须能够替换它的父类,而程序行为不变。

反例:有一个"鸟"类,子类"企鹅"不会飞,却被迫实现了 fly()

class Bird:
    def fly(self):
        return 'Flying in the sky.'


class Penguin(Bird):
    def fly(self):
        # 企鹅根本不会飞!
        raise NotImplementedError('Penguin cannot fly!')

如果某段代码拿着 Bird 引用调用 fly(),遇到 Penguin 时程序就崩了。这不是好的继承设计。

正例:将"飞的行为"从 Bird 中移除,换成更细粒度的设计:

class Bird:
    def eat(self):
        return 'Eating.'


class FlyingBird(Bird):
    def fly(self):
        return 'Flying.'


class Sparrow(FlyingBird):
    pass


class Penguin(Bird):
    def swim(self):
        return 'Swimming.'

Sparrow 会飞,所以它继承 FlyingBirdPenguin 不会飞,它只继承 Bird,另加一个 swim()。这样,任何接受 FlyingBird 的函数都不会担心拿到一只不会飞的企鹅。这里要特别注意:使用继承前,检验"子类是否真的能替换父类"是必须做的功课

5.4 I — 接口隔离原则

一句话:不应该强迫一个类依赖它不使用的方法。

简单来说:接口要小而精,不要大而全

反例:一个庞大的"宠物服务"接口(Python 中用类模拟):

class PetService:
    def groom(self):
        raise NotImplementedError

    def feed(self):
        raise NotImplementedError

    def breed(self):
        raise NotImplementedError

    def train(self):
        raise NotImplementedError

子类 DogService 继承了这个接口,但"繁殖"和"训练"它根本用不上,却被迫实现了空方法。这是对子类的"债"。

正例:拆分成细粒度接口:

class Groomable:
    def groom(self):
        raise NotImplementedError

class Feedable:
    def feed(self):
        raise NotImplementedError

class Trainable:
    def train(self):
        raise NotImplementedError


class DogService(Groomable, Feedable):
    def groom(self):
        return 'Grooming the dog.'

    def feed(self):
        return 'Feeding the dog.'

DogService 只继承它真正需要的接口。干净、清晰、没有包袱。这跟前面讲的"多重继承"呼应了:Python 中接口隔离常常通过 Mix-in 来实现。

5.5 D — 依赖倒置原则

一句话:高层模块不应该依赖低层模块,两者都应该依赖抽象。

说人话就是:代码应该依赖"抽象的接口",而不是"具体的实现"

反例:宠物店系统直接依赖 MySQL:

class MySQLDatabase:
    def save(self, data):
        print('Saving to MySQL.')


class PetStore:
    def __init__(self):
        # 硬编码依赖 MySQL
        self.db = MySQLDatabase()

    def add_pet(self, pet):
        self.db.save(pet)

如果有一天老板说"我们改用 PostgreSQL",PetStore 就要被修改。依赖太具体,灵活性就消失了。

正例:抽象出 Database 接口,让 PetStore 只依赖接口:

class Database:
    def save(self, data):
        raise NotImplementedError


class MySQLDatabase(Database):
    def save(self, data):
        print('Saving to MySQL.')


class PostgreSQLDatabase(Database):
    def save(self, data):
        print('Saving to PostgreSQL.')


class PetStore:
    def __init__(self, db: Database):
        # 依赖抽象,而不是具体实现
        self.db = db

    def add_pet(self, pet):
        self.db.save(pet)


# 切换数据库时,只需要传入不同的对象
store = PetStore(MySQLDatabase())
store.add_pet('Buddy')

store = PetStore(PostgreSQLDatabase())
store.add_pet('Buddy')

PetStore 只认 Database 这个抽象,具体是 MySQL 还是 PostgreSQL,它不在乎。这就是"依赖倒置"——依赖关系从"指向具体"反转为"指向抽象"。

5.6 SOLID 原则在宠物店系统中的应用

小王听完后,把宠物店系统重新过了一遍:

  • 单一职责:把"宠物数据存储"和"支付服务"分离成两个独立的类,以后改支付不会再破坏宠物数据。
  • 开闭原则:新增宠物类型时,新写一个 Animal 子类即可,不修改现有代码。
  • 里氏替换:重新检讨继承关系,确保 Dog 真的可以完全替换 Animal 使用。
  • 接口隔离:把"会叫、会游泳、会飞"拆成细粒度 Mix-in,各个宠物按需继承。
  • 依赖倒置:把数据库、邮件服务等依赖抽象化,这样更换底层服务不必动业务代码。

以下是一个综合的 SOLID 原则示例——宠物店收款模块:

from abc import ABC, abstractmethod


# 抽象:支付渠道接口(依赖倒置 + 接口隔离)
class PaymentChannel(ABC):
    @abstractmethod
    def pay(self, amount):
        pass


class WeChatPay(PaymentChannel):
    def pay(self, amount):
        return f'微信支付 {amount} 元'


class Alipay(PaymentChannel):
    def pay(self, amount):
        return f'支付宝支付 {amount} 元'


# 单一职责:订单类只负责订单信息
class Order:
    def __init__(self, total):
        self.total = total
        self.status = '待支付'


# 单一职责 + 依赖倒置:收银台只负责收款,不关心具体渠道
class Cashier:
    def checkout(self, order: Order, channel: PaymentChannel):
        result = channel.pay(order.total)
        order.status = '已支付'
        return result


# 使用(将来增加新支付渠道,只需新增一个 PaymentChannel 子类)
cashier = Cashier()
order = Order(200)

print(cashier.checkout(order, WeChatPay()))  # 微信支付 200 元
print(order.status)  # 已支付
小结

SOLID 不是教条,是五种"品味好代码"的角度。当你感到某个类越写越大,就是在提醒你"单一职责";当你要修改已经上线了的代码,就是在提醒你"开闭原则";当你担心子类能不能替换父类,就是在应用"里氏替换";当接口庞大臃肿时,就是"接口隔离";当你硬编码依赖某项具体技术时,就是"依赖倒置"。练习久了,这些原则会内化成你的直觉。


六、Python 现代 OOP 特性

Python 从未停止进化。3.7 版本之后,Python 为标准库添加了多项面向对象编程的现代化工具,它们让类定义更加简洁、类型表达更加清晰、内存使用更加高效。作为现代 Python 开发者,这些特性值得你认真掌握。

6.1 dataclass:告别样板代码

故事:定义数据类太枯燥了

宠物店系统中有大量"数据类"——宠物档案、会员信息、订单记录。它们的特点是:主要存放数据,方法很少。传统写法的样板代码多到令小王抓狂:

class Pet:
    def __init__(self, name, age, breed):
        self.name = name
        self.age = age
        self.breed = breed

    def __repr__(self):
        return f'Pet(name={self.name!r}, age={self.age!r}, breed={self.breed!r})'

    def __eq__(self, other):
        if not isinstance(other, Pet):
            return NotImplemented
        return (self.name == other.name
                and self.age == other.age
                and self.breed == other.breed)

仅仅三个属性,就写出了这么多基础设施代码。Pet 的核心其实是 nameagebreed 这三个字段,而 __init____repr____eq__ 都是"体力活"。

Python 3.7 引入了 dataclass 装饰器,可以自动生成这些样板方法:

from dataclasses import dataclass


@dataclass
class Pet:
    name: str
    age: int
    breed: str


buddy = Pet('Buddy', 3, 'Golden Retriever')
whiskers = Pet('Whiskers', 1, 'British Shorthair')

print(buddy)              # Pet(name='Buddy', age=3, breed='Golden Retriever')
print(buddy == Pet('Buddy', 3, 'Golden Retriever'))  # True

仅仅五行代码,自动获得了 __init____repr____eq__ 等方法。代码更聚焦于"业务字段",而不是基础设施。

dataclass 的进阶用法

dataclass 远不止自动生成方法那么简单,它还支持:

from dataclasses import dataclass, field
from typing import List


@dataclass
class MemberAccount:
    username: str
    balance: float = 0.0                  # 默认值
    tags: List[str] = field(default_factory=list)  # 用工厂函数创建独立列表
    frozen_id: int = field(default=0, repr=False)  # 不参与 repr


@dataclass(frozen=True)
class Point:
    """不可变数据类:一旦创建就不能修改"""
    x: int
    y: int


account = MemberAccount('alice')
print(account)  # MemberAccount(username='alice', balance=0.0, tags=[])

p1 = Point(3, 4)
# p1.x = 10  # 这行会报错:FrozenInstanceError

# frozen=True 让数据类成为不可变的(类似元组),可以做字典的 key
point_map = {Point(3, 4): 'A点'}
print(point_map[Point(3, 4)])  # A点

关键点解释:

  • default_factory:当默认值必须是可变对象(如列表、字典)时使用。不要写 tags: List[str] = [],那样所有实例都会共享同一个列表!
  • frozen=True:创建不可变的数据类。它让数据类的实例像元组一样,既安全又能做哈希键。
dataclass 与普通 class 的选择

小李曾问李哥:“既然 dataclass 这么好,是不是所有类都该用 dataclass 定义?”

李哥回答:“不是。 dataclass 适合’数据容器’——字段多、逻辑少、主要在存取数据的类。如果你的类有复杂的业务逻辑、生命周期管理、状态机等,普通类更合适。记住一个原则:让工具匹配问题,而不是让问题迁就工具。

6.2 类型注解与运行时检查

故事:暗藏的类型错误

Python 是动态类型语言,变量可以指向任何类型的对象。这在带来灵活性的同时,也造成了维护上的困扰:

def feed_pet(pet, food_amount):
    return pet.name + ' eats ' + food_amount + ' grams.'

如果有人不小心传入了:

result = feed_pet(dog, '很多')  # 参数类型完全错误

程序不会立刻报错,但逻辑已经错了。更隐蔽的是,food_amount 本应该是数值,却在某处被粗心大意的开发者传成了字符串,结果下游所有数值计算都产生了偏差。

类型注解:让意图浮出水面

Python 从 3.5 开始支持类型注解(Type Hints)

def feed_pet(pet: str, food_amount: int) -> str:
    return f'{pet} eats {food_amount} grams of food.'

参数后面的 : str: int 是类型注解,括号外 -> str 声明了返回值类型。它们不会在运行时强制检查,但意义重大:

  1. IDE 能提供智能补全和错误提示;
  2. 配合静态检查工具(如 mypy),在提交代码前捕获类型错误;
  3. 作为团队协作的无形文档,让类型意图一目了然。
类的类型注解进阶

Python 3.10 及以后,可以用 X | Y 语法表达联合类型,更简洁:

from typing import Optional


class Dog:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def get_owner(self) -> Optional[str]:
        """返回主人姓名,可能为 None"""
        return self._owner


def find_friend(dog: Dog, friends: list[Dog]) -> Dog | None:
    """在朋友列表中找一个同品种的狗,可能找不到"""
    for friend in friends:
        if friend.breed == dog.breed:
            return friend
    return None
Protocol:鸭子类型的静态版本

Protocol 是 Python 3.8 引入的结构化子类型工具。它让你在不强制继承的情况下,定义"接口约束":

from typing import Protocol


class Pet(Protocol):
    name: str

    def speak(self) -> str:
        ...


class Dog:
    name = 'Buddy'

    def speak(self) -> str:
        return 'Woof!'


class Car:
    def drive(self) -> str:
        return 'Vroom!'


def announce(pet: Pet) -> str:
    return f'{pet.name}: {pet.speak()}'


print(announce(Dog()))  # OK
# announce(Car())  # mypy 会报错:Car 不满足 Pet 协议

注意:Dog 并没有显式继承 Pet,但由于它"长得像 Pet"(有 name 属性和 speak() 方法),就被视为满足 Pet 协议。这就是结构化子类型——Python 鸭子类型的"静态版本"。Car 虽然也有名字,但缺少 speak() 方法,因此 mypy 会拒绝它。

Protocol 的实际价值

当你在设计一个松耦合系统时,Protocol 让你表达"我需要一个会叫的东西",而不是"我需要一个动物子类"。任何满足接口的对象——不管它的继承树长什么样——都可以被接受。这大大提高了代码的灵活性。

6.3 slots:优化内存占用

故事:百万级数据行的挑战

宠物店系统上线一年后,发展成了一个连锁品牌,每天要处理百万级订单。小王的系统突然变得很慢,经分析发现:创建大量对象时,内存占用非常惊人。为什么?

默认情况下,Python 对象使用一个名为 __dict__ 的字典来存储属性。这个字典带来了灵活性——你可以随时随地给对象添加新属性。但灵活性是有代价的:每个对象都背着一个字典,内存开销巨大。

class Pet:
    def __init__(self, name, age, breed):
        self.name = name
        self.age = age
        self.breed = breed


# 可以动态添加属性,非常灵活
buddy = Pet('Buddy', 3, 'Golden Retriever')
buddy.favorite_toy = 'ball'  # 合法
print(buddy.__dict__)  # {'name': 'Buddy', 'age': 3, 'breed': 'Golden Retriever', 'favorite_toy': 'ball'}

但如果你的系统要创建百万个 Pet 对象,每个对象都背着一个 __dict__,内存占用就变得无法接受。

slots 定制属性存储

__slots__ 允许你显式声明一个类的属性列表,Python 会改用固定大小的数组来存储属性,而不是字典:

class PetWithSlots:
    __slots__ = ('name', 'age', 'breed')

    def __init__(self, name, age, breed):
        self.name = name
        self.age = age
        self.breed = breed


buddy = PetWithSlots('Buddy', 3, 'Golden Retriever')
print(buddy.name)  # Buddy

# 代价:不能再动态添加新属性
try:
    buddy.favorite_toy = 'ball'
except AttributeError as e:
    print(e)  # 'PetWithSlots' object has no attribute 'favorite_toy'

__slots__ 的收益是显著的:根据官方文档和社区测试,对于简单数据对象,使用 __slots__ 后内存占用通常可以减少 40%~50%。在创建大量实例的场景下(如解析百万级数据行、游戏中的粒子系统、金融领域的订单簿),这是非常关键的内存优化手段。

同时使用 slots 和 dataclass

好消息是:dataclass 也支持 __slots__

from dataclasses import dataclass


@dataclass(slots=True)  # Python 3.10+ 支持
class Pet:
    name: str
    age: int
    breed: str


buddy = Pet('Buddy', 3, 'Golden Retriever')
print(buddy)  # Pet(name='Buddy', age=3, breed='Golden Retriever')

# 和普通 dataclass 一样,但不能动态添加属性
# buddy.favorite_toy = 'ball'  # AttributeError

@dataclass(slots=True) 结合了两者的优点:既有自动生成的样板方法,又有节省内存的底层存储。这是 Python 3.10+ 为数据处理密集型应用提供的利器。

slots 的使用注意事项
  1. __slots__ 不能阻止弱引用(__weakref__ 需要显式加入)——一般用不到,先知道即可。
  2. 子类如果也定义了 __slots__,不会重复父类的槽;但如果不定义 __slots__,子类会重新获得 __dict__
  3. __slots__ 优化的是大量对象的内存场景。如果你的程序只创建几十个对象,用不用 __slots__ 差别不大,不必为了优化而优化。
小结

dataclass 帮你自动生成样板代码,Protocol 帮你定义结构化接口,__slots__ 帮你压缩内存占用。它们是 Python 现代化 OOP 的三大武器。在实际项目中,合理搭配使用这些特性,会让你的代码同时具备简洁、安全和高效三张名片。


总结:小王的新起点

一年后,宠物店管理系统已经从单一的宠物管理发展成了一个包含会员储值、智能推荐、库存管理、数据分析的连锁门店平台。更重要的是,小王已经从那个用字典堆砌数据的实习生,成长为团队里的 Python 技术骨干。

回到最初那个问题:面向对象编程到底解决了什么?

它不是魔法,也不会让你写的代码自动变好。它真正的价值,在于提供了一种组织复杂系统的思维框架

  • 类与对象把数据和操作数据的逻辑打包在一起,让代码结构贴合人类认知;
  • 封装保护数据不被肆意修改,让业务规则落在该在的地方;
  • 继承与多态实现代码复用与行为抽象,让系统在需求变化时依然从容;
  • 设计模式把常见问题的解法沉淀为可复用的套路;
  • SOLID 原则作为标尺,检视并改善设计质量;
  • dataclass、Protocol、__slots__ 这些现代化特性,为代码在简洁性、安全性和性能之间找到平衡。

李哥最后送给小王的一句话,也送给此刻读到这里的你:

面向对象编程的精髓不在于你会写 class,而在于你学会了用"对象"的眼光重新观察这个世界。

当你再遇到一个复杂的业务需求时,不妨先停下来问自己三个问题:

  1. 这个系统里有哪些"东西"?它们就是候选的
  2. 每个"东西"拥有哪些数据行为?它们就是类的属性和方法。
  3. 这些"东西"之间是什么关系?是"是一种"(继承)还是"有一个"(组合)?

想清楚这三点,再动手写代码,你会发现很多难题在落笔之前就已经解决了。祝你在 Python OOP 的世界里,写出清晰、优雅、可维护的代码。我们下篇文章见。

更多推荐