Python:里氏替换原则(LSP)

里氏替换原则(Liskov Substitution Principle, 简称 LSP)是面向对象设计(OOD)与 SOLID 原则中的重要组成部分,由 Barbara Liskov 在 1987 年提出。它是继承体系设计的根本规范,决定了父类与子类之间是否真正具有"可替换性"。

简而言之:

只要父类可以使用的地方,子类也必须能够安全地替代父类,而不会破坏程序功能。

LSP 是保证继承层次结构稳定、可维护、可扩展的基础。如果 LSP 被破坏,继承就会带来更多问题而非好处。

一、为什么需要里氏替换原则?

继承本质上表达"是一种"(is-a)关系,例如:

• Dog 是一种 Animal

• SavingsAccount 是一种 Account

• AdminUser 是一种 User

然而,不正确的继承会导致:

• 子类行为违背父类预期

• 子类重写方法改变语义

• 程序运行时出错或表现异常

• 继承层次难以维护或扩展

LSP 的意义就是确保继承结构的正确性,使子类是真正的父类扩展,而不是破坏父类。

二、里氏替换原则的形式化定义

Barbara Liskov 给出的经典定义:

如果 S 是 T 的子类型,那么类型 T 的对象可以被类型 S 的对象替换,而不会改变程序的正确性。

换句话说:

• 子类必须遵守父类规定的行为契约

• 子类不能削弱父类的行为

• 子类可以扩展功能,但不能改变父类的语义

这是一种行为约束(Behavioral Subtyping)。

想象一下,你平时用的插座都是标准的三孔插座。如果某个电器插头改变了尺寸或形状,即便是同类型的电器,也可能无法直接插入使用。

里氏替换原则就像"标准插头规范":任何子类都必须遵守父类定义的接口和行为规范,才能在父类能工作的场景中无缝替换使用。

遵循 LSP,继承就像标准插座一样稳健可靠,子类可以扩展功能,但不能破坏已有"兼容性"。

三、Python 与 LSP 的关系

Python 是动态语言,没有强制类型检查,因此:

• Python 不会阻止违反 LSP 的继承

• 行为契约完全依赖开发者自己遵守

• 违反 LSP 在运行期才会暴露问题(例如 AttributeError, TypeError, Logic Bug 等)

因此在 Python 中更需遵循 LSP 来构建健壮的类体系。

(1)经典的违反 LSP 的例子

方形"继承"矩形:

python 复制代码
class Rectangle:    def __init__(self, w, h):        self.w = w        self.h = 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       def set_height(self, h):        self.w = self.h = h

表面上 "方形是一种矩形",但行为上的契约并不一致:

apache 复制代码
def test(rect):    rect.set_width(4)    # 预期:只改宽度,高度不变    rect.set_height(5)   # 预期:只改高度,宽度不变    print(rect.area())   # 预期:4 * 5 = 20
test(Rectangle(2, 3))    # → 正常输出 20test(Square(2, 2))       # → 行为完全改变!输出 25

说明:

父类 Rectangle 的隐含契约:

apache 复制代码
# 对于 Rectangle,下面的操作是独立的:rect.set_width(4)   # 只修改宽度rect.set_height(5)  # 只修改高度# 结果:面积 = 4 * 5 = 20

子类 Square 破坏了这一契约:

apache 复制代码
# 对于Square,这些操作不再独立:square.set_width(4)   # 宽度=4, 高度=4 (宽度修改影响了高度)square.set_height(5)  # 宽度=5, 高度=5 (高度修改影响了宽度)# 结果:面积 = 5 * 5 = 25 (不是预期的20)

Square 改变了父类 Rectangle 的关键行为契约(宽高修改的独立性),导致在需要 Rectangle 的场景中无法正确替换 Rectangle,从而违反了 LSP。

(2)正确的设计方式

将 Rectangle 与 Square 设计为并列关系,继承抽象类 Shape:

ruby 复制代码
from abc import ABC, abstractmethod
class Shape(ABC):    @abstractmethod    def area(self):        pass

class Rectangle(Shape):    def __init__(self, w, h):        self.w = w        self.h = 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 * self.side

说明:

二者都是 Shape,且不互为子类,因此不会互相破坏契约。

这是工业界最常采用且最安全的设计。

四、Python 中违反 LSP 的常见场景

(1)子类重写方法,但改变了父类语义,即子类方法的行为与父类预期不一致

ruby 复制代码
class FileWriter:    def write(self, text):        # 写入文件        pass
class ConsoleWriter(FileWriter):    def write(self, text):        # ❌ 改成打印到屏幕,破坏语义        print(text)

改进:创建 Writer 抽象基类;不要让 ConsoleWriter 继承具体实现类。

(2)子类增加方法参数,导致不能替代父类

ruby 复制代码
class Animal:    def speak(self):        print("动物叫")
class Dog(Animal):    def speak(self, volume):  # ❌ 额外增加参数        print("汪" * volume)

改进:保持参数一致,或另加方法。

(3)子类限制更多条件,破坏父类契约

ruby 复制代码
class Account:    def withdraw(self, amount):  # 允许任何正数        pass
class VIPAccount(Account):    def withdraw(self, amount):        if amount > 5000:        # ❌ 限制更严格            raise ValueError("VIP 不允许取太多?")

子类不能比父类更严格;必须遵守父类契约。

五、遵守 LSP 的设计建议

(1)继承只表达"is-a"关系

不要滥用继承,将子类设计成真正的父类扩展,而非简单重用代码。

(2)子类行为必须符合父类预期语义

子类方法的功能和行为应与父类保持一致,不可任意改变。

(3)输入输出保持兼容

子类不能收紧父类的输入要求,也不能改变父类的输出预期。

(4)方法重写保持逻辑兼容

子类重写方法时,应在不破坏父类契约的前提下扩展功能。

(5)优先使用组合而非继承

当"有一个"(has-a)关系更合理时,使用组合替代继承,以提高灵活性和可维护性。

(6)使用抽象基类定义行为契约

通过 ABC(Abstract Base Class)明确方法接口和行为规范,确保子类遵守契约。

(7)文档与注释明确规范

在文档或代码注释中清晰描述方法行为,便于子类开发者理解并遵循父类契约。

六、综合示例:符合 LSP 的日志系统

python 复制代码
from abc import ABC, abstractmethod
class Logger(ABC):    @abstractmethod    def log(self, msg):        pass

class FileLogger(Logger):    def log(self, msg):        with open("log.txt", "a", encoding="utf-8") as f:            f.write(msg + "\n")

class ConsoleLogger(Logger):    def log(self, msg):        print(msg)

def process(logger: Logger):    logger.log("开始处理任务...")    logger.log("任务完成!")

process(ConsoleLogger())  # OKprocess(FileLogger())     # OK

说明:

• FileLogger 与 ConsoleLogger 都遵守 Logger 的契约

• 任意一个都可以替代 Logger

• 真正符合 LSP

📘 小结

里氏替换原则是继承体系设计的基础,它要求子类必须能够完全替代父类,而不改变程序行为。遵守 LSP 能提升代码稳定性、可维护性和扩展能力;违反 LSP 的继承会导致隐藏 bug、错误行为和脆弱的结构。在 Python 中没有编译时强类型约束,因此更应通过约定、抽象基类和清晰的设计来确保子类遵守父类行为契约。遵循 LSP 是编写健壮、可扩展面向对象代码的重要前提。

"点赞有美意,赞赏是鼓励"

相关推荐
聊天QQ:276998852 天前
车桥耦合matlab程序。 使用newmark法进行数值积分,考虑不平顺车辆-无砟轨道-桥梁耦...
里氏替换原则
沟通QQ:276998855 天前
基于多能互补的热电联供型微网优化运行探索
里氏替换原则
烤麻辣烫17 天前
23种设计模式(新手)-5里氏替换原则
java·学习·设计模式·intellij-idea·里氏替换原则
口袋物联25 天前
图解码说-六大设计原则(开闭原则、单一职责原则、里氏替换原则、接口隔离原则、依赖倒置原则、迪米特法则)
接口隔离原则·依赖倒置原则·里氏替换原则·开闭原则·单一职责原则·设计模式原则·迪米特法原则
玩机达人882 个月前
三星S25Ultra/S24安卓16系统Oneui8成功获取完美root权限+LSP框架
android·linux·里氏替换原则
云点一点点2 个月前
完全理解您的要求。我将输出一个关于MySQL的原创文章标题。MySQL并发控制的幕后锁、事务隔离级别与性能优化实战
里氏替换原则
土了个豆子的3 个月前
02.继承MonoBehaviour的单例模式基类
开发语言·visualstudio·单例模式·c#·里氏替换原则
小蜗牛在漫步3 个月前
设计模式六大原则2-里氏替换原则
设计模式·里氏替换原则
WISHMELUCK1'5 个月前
设计模式的六大设计原则
设计模式·接口隔离原则·依赖倒置原则·里氏替换原则·迪米特法则·合成复用原则·单一职责原则