python:面向对象编程的设计原则之里氏替换原则

python:面向对象编程的设计原则之里氏替换原则,第1张

文章目录
  • 一,再说继承
    • (一)真实的继承
    • (二)python没有重载与重写
    • (三)子类使用父类中的方法
    • (四)组合
  • 二,里氏替换原则
    • (一)什么是里氏替换原则
    • (二)一个经典的违反里氏替换原则的例子:正方形不是矩形

一,再说继承 (一)真实的继承

在前面讲过,继承这一概念的本质就是依据 MRO 对类树进行的一个搜索动作,它在使用 . 的时候被触发,结合类中的构造方法 __init__ 与类方法中的 self 参数,就为多态原理提供了基础,从而使得对象能根据不同的环境体现出不同的状态。


实际上,继承没有多玄乎,它就是 OOP 中实现代码复用的基本方式,无论是什么设计原则,继承都是实现它们的基础。


看一个在继承中定制构造函数的例子:

class Employee:  # 这是一个雇员
    def __init__(self, name=None, salary=None):
        self.name = name
        self.salary = salary

    def work(self):
        print(self.name, "does stuff")


class Server(Employee):  # 服务员是一个雇员
    def __init__(self, name=None, salary=None, gender=None):
        Employee.__init__(self, name=name, salary=salary)
        self.gender = gender

    def work(self):
        print(self.name, "interfaces with customer")


class Chef(Employee):  # 主厨是一个雇员
    def __init__(self, name=None, salary=None, gender=None):
        Employee.__init__(self, name=name, salary=salary)
        self.gender = gender

    def work(self):
        print(self.name, "makes food")


if __name__ == '__main__':
    server_one = Server(name='Server one', salary=1000)
    server_one.work()

    server_two = Server(name='Server two', salary=2000)
    server_two.work()

    chef = Chef(name="Thomas Johnson", salary=10000)
    chef.work()

>>>
Server one interfaces with customer
Server two interfaces with customer
Thomas Johnson makes food
  • 代码中有些继承关系。


  • 在子类的构造函数中首先通过 ParentClassName.__init__(*args, **kwargs) 的方式显式地使用父类中的初始化动作,然后在子类中初始化扩展出来的的属性。


(二)python没有重载与重写

学过 Java 的应该知道重写(Overriding)和重载(Overloading)的概念:

  • 重载要求一个类内部允许有两个同名方法,只要参数数量、参数类型和返回类型不同,那么都能够被调用。


  • 重写要求子类中可以有与父类中方法同名的方法,但参数数量、参数类型和返回类型都须保持一致。


但就 python 来说:

  • 由于只认类中同名方法的最后一个实现(无论参数差异),自然就不存在重载这一说法。


  • 由于 python 是一门动态语言,并不要求在声明变量时显式指定类型,且能通过 *args**kwargs 来传递任意数量的参数,自然就不存在严格意义的重写。


但是,python 实际上扩展了重写的含义,使得一般意义上的子类中修改父类方法的做法就是扩展了的重写。


举个例子🌰:

class Employee:  # 这是一个雇员
    def __init__(self, name=None, salary=None):
        self.name = name
        self.salary = salary

    def work(self):
        print(self.name, "does stuff")


class Server(Employee):  # 服务员是一个雇员
    def __init__(self, gender=None, **kwargs):
        super(Server, self).__init__(**kwargs)
        self.gender = gender

    def work(self, saying):
        print(self.name, f"interfaces with customer in the morning, just say '{saying}'")

    def work(self, saying):
        print(self.name, f"interfaces with customer in the afternoon, just say '{saying}'")


if __name__ == '__main__':
    server = Server(name="张三", salary=10000, gender='man')
    print(server.__dict__)
    server.work('good morning')

>>>
{'name': '张三', 'salary': 10000, 'gender': 'man'}
张三 interfaces with customer in the afternoon, just say 'good morning'
  1. 子类允许完全重写父类方法。


  2. 类中方法只执行最后一个实现。


(三)子类使用父类中的方法

相比上面在子类中重写构造方法的做法,使用 super().__init(*args, **kwargs)__ 来实现各类方法的调用是更加明智的做法。


因为 super 的存在,充分利用了 python 底层的实现原理,让多重继承和钻石继承路线更加明确。


super 容易让人困惑的一点就是在面对钻石继承时,它是如何在 MRO 上起作用的。


看一个例子来简单了解 super 的工作方式:

class A:
    def act(self, msg):
        print(f'{msg} --> A')
        print(f'{msg} <-- A')


class B:
    def act(self, msg):
        print(f'{msg} --> B')
        print(f'{msg} <-- B')


class C(A):
    def act(self, msg):
        print(f'{msg} --> C')
        super(C, self).act('C')
        print(f'{msg} <-- C')


class D(A, B):
    def act(self, msg):
        print(f'{msg} --> D')
        super(D, self).act('D')
        print(f'{msg} <-- D')


class E(B, A):	# 注意与D的继承关系的区别
    def act(self, msg):
        print(f'{msg} --> E')
        super(E, self).act('E')
        print(f'{msg} <-- E')


class F(C):
    def act(self, msg):
        print(f'{msg} --> F')
        super(F, self).act('F')
        print(f'{msg} <-- F')


class G(C):
    def act(self, msg):
        print(f'{msg} --> G')
        super(C, self).act('G')	# 注意与F的super起点的区别
        print(f'{msg} <-- G')


if __name__ == '__main__':
    print([x.act('start') for x in [A(), B(), C(), D(), E(), F(), G()]])

>>>
start --> A
start <-- A
start --> B
start <-- B
start --> C
C --> A
C <-- A
start <-- C
start --> D
D --> A
D <-- A
start <-- D
start --> E
E --> B
E <-- B
start <-- E
start --> F
F --> C
C --> A
C <-- A
F <-- C
start <-- F
start --> G
G --> A
G <-- A
start <-- G
[None, None, None, None, None, None, None]

首先来捋一下类的继承树:

再看一下各个类的方法解析顺序:

>>> pprint([x.__mro__ for x in [A, B, C, D, E, F, G]])
>>> 
[(<class '__main__.A'>, <class 'object'>),
 (<class '__main__.B'>, <class 'object'>),
 (<class '__main__.C'>, 
  <class '__main__.A'>, 
  <class 'object'>),
 (<class '__main__.D'>,
  <class '__main__.A'>,
  <class '__main__.B'>,
  <class 'object'>),
 (<class '__main__.E'>,
  <class '__main__.B'>,
  <class '__main__.A'>,
  <class 'object'>),
 (<class '__main__.F'>,
  <class '__main__.C'>,
  <class '__main__.A'>,
  <class 'object'>),
 (<class '__main__.G'>,
  <class '__main__.C'>,
  <class '__main__.A'>,
  <class 'object'>)]

最后看看 super 的作用效果:

  1. C 中 super(C, self).act(‘C’) 的效果:

  2. D 中 super(D, self).act(‘D’) 的效果:

  3. E 中 super(E, self).act(‘E’) 的效果:

  4. F 中 super(F, self).act(‘F’) 的效果:

  5. G 中 super(A, self).act(‘G’) 的效果:

super([type[, object-or-type]])函数 就是利用 MRO 机制在类书中按顺序搜索第一个符合条件的类属性或类方法。


  • 搜索会从 type 之上的父类开始。


  • object-or-type 确定用于搜索的 MRO。


改造一下原来的代码:

class Employee:  # 这是一个雇员
    def __init__(self, name=None, salary=None):
        self.name = name
        self.salary = salary

    def work(self):
        print(self.name, "does stuff")


class Server(Employee):  # 服务员是一个雇员
    def __init__(self, gender=None, **kwargs):
        super(Server, self).__init__(**kwargs)
        self.gender = gender

    def work(self):
        print(self.name, "interfaces with customer")


class Chef(Employee):  # 主厨是一个雇员
    def __init__(self, gender=None, **kwargs):
        super(Chef, self).__init__(**kwargs)
        self.gender = gender

    def work(self):
        print(self.name, "makes food")


class Robot(Chef):  # 披萨机器人是一个特殊主厨
    def __init__(self, name=None, working=None):
        super(Robot, self).__init__(name=name, salary=None)
        self.working = working

    def work(self):
        print(self.name, f"{self.working}")


if __name__ == '__main__':
    chef = Chef(name="张三",salary=10000, gender='man')
    print(chef.__dict__)

    pizza_robot = Robot(name='PizzaRobot', working='makes pizza')
    print(pizza_robot.__dict__)


>>>
{'name': '张三', 'salary': 10000, 'gender': 'man'}
{'name': 'PizzaRobot', 'salary': None, 'gender': None, 'working': 'makes pizza'}

python doc: super([type[, object-or-type]])
super confusing python multiple inheritance super()
Python’s super() considered super!
Python super函数详解

(四)组合

继承提供的一个好处就是,只要父类设计合理,子类就能够在父类的基础上进行功能扩展而无需修改父类。


尽管我们能通过多重继承和多继承创建足够复杂的对象,不断扩展 is-a 链,但是这条长长的继承链产生的原因之一,可能仅仅是我们需要在链条的末端拥有链中的某些接口。


这往往容易违背接口隔离原则,导致孙子对象拥有祖先对象中不必要的一些接口(比如前面 Robot 继承得却不使用的 salery 和 gender)。


如果这些接口恰好绝大多数都在链的某个类中,我们完全可以让子类直接舍去中间环节而直接继承该类,然后写一些该类没有的扩展。


另一种实现该类复用代码的方式,就是直接通过组合该类的方式来获得这些接口,然后写一些该类没有的扩展:

组合将产生 has-a 关系,这种横向扩展的关系有利于创建拥有比较复杂结构的对象:

class Employee:  # 这是一个雇员
    def __init__(self, name=None, salary=None):
        self.name = name

    def work(self):
        print(self.name, "does stuff")

...

class Robot:
    def __init__(self, name, working=None):
        self.entry = Employee(name=name)
        self.working = working
        # 机器人使用次数
        self.count = 3

    def work(self):
        if self.count == 0:
            print(self.entry.name, f"can not work")
            return
        self.count -= 1
        print(self.entry.name, f"{self.working}")
        print(f"LastCount of {self.entry.name}: {self.count}")


if __name__ == '__main__':
    pizza_robot = Robot(name='PizzaRobot', working='makes pizza')
    print(pizza_robot.entry.name)
    pizza_robot.work()
    pizza_robot.work()
    pizza_robot.work()
    pizza_robot.work()
    pizza_robot.work()

>>>
PizzaRobot
PizzaRobot makes pizza
LastCount of PizzaRobot: 2
PizzaRobot makes pizza
LastCount of PizzaRobot: 1
PizzaRobot makes pizza
LastCount of PizzaRobot: 0
PizzaRobot can not work
PizzaRobot can not work

当然,使用 is-a 还是 has-a 是一个设计问题,与具体的对象的关系紧密相关。


二,里氏替换原则

前面说了一些东西,只是想说明可提高代码复用性的一些基础,而如何有效地设计并高效复用代码却是一项有挑战的工作,因此才诞生了所有的设计原则、设计模式等概念来做一些指导性的工作,但继承无疑是一个重要的基础。


由于 python 支持多重继承与多继承,相较如 Java 之类的面向对象的语言更加容易产生继承滥用问题。



尽管可以通过事先进行合理设计来尽量避免继承滥用问题,但却并没有稍微清晰的指导性原则,而里氏替换原则的工程化应用就能帮助解决问题。


(一)什么是里氏替换原则

Barbara Liskov 和 Jeannette Wing 在一篇论文中从计算机科学的学术角度简述了里氏替换原则(Liskov Substitution Principle,LSP):

子类型要求:设φ ( x ) 是关于 T 类型的对象 x 的可证明性质,那么φ ( y ) 对于 S 类型的对象 y 应该为真,其中 S 是 T 的子类型。


站在简单的应用角度来看,这个原理可以这样理解:

子类对象应该能够在不破坏应用程序完整性的情况下替换父类对象。


最简单地说,就是:

子类可以替换父类,但反之不成立。


如果子类都能替换父类了,那为什么还要子类化以实现功能扩展?这么想显然不对。



因为一般所谓的功能扩展,多是在子类中实现父类所没有的接口(属性与方法)。


而且从多态概念的角度来看,对父类方法的重写显然也应该是功能扩展的一种方式。



非常简单地理解,里氏替换原则并不是说子类就不能重写父类方法,而是应该与父类该方法拥有相同的期望。


这个“相同的期望”显得比较难理解。


因为里氏替换原则本身讲的是比较抽象的类型系统的事,只不过我们这里将它简化、具化到 OOP 中类的关系上,从而与继承、多态等概念关联到了一起。



所谓的“相同的期望”,应该就是子类与父类在功能性上所产生的最小交集,正是这个交集的存在,才使得子类能够替代暂时使用中的父类,从而实现后续的功能扩展,以达到真正地复用代码的目的,实现可扩展的、健壮的系统,

Wikipedia:Liskov substitution principle
细说 里氏替换原则
SOLID Principles : The Liskov Substitution Principle
里氏替换原则(Liskov Substitution Principle)
The Liskov Substitution Principle, and Python
What is an example of the Liskov Substitution Principle?
The Liskov substitution principle

(二)一个经典的违反里氏替换原则的例子:正方形不是矩形

从数学概念上讲,正方形是特殊的矩形。


就求面积这种计算而言,两者本质都是一样的:对边 * 邻边。



在 OOP 中用正方形继承长方形,改变长或宽的长度后再计算面积的话,就会出现问题:

class Rectangle:
    def __init__(self, height, width):
        self._height = height
        self._width = width

    @property   # 只读属性
    def width(self):
        return self._width

    @width.setter   # 可写属性
    def width(self, value):
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        self._height = value

    def get_area(self):
        return self._width * self._height



class Square(Rectangle):
    def __init__(self, size):
        super(Square, self).__init__(self, size)    # 调用父类的构造函数

    @Rectangle.width.setter 
    def width(self, value):
        self._width = value
        self._height = value

    @Rectangle.height.setter
    def height(self, value):
        self._width = value
        self._height = value



def get_squashed_height_area(Rectangle):
    Rectangle.height = 1    # 重新设置高度为1
    area = Rectangle.get_area()   # 获取面积
    return area


if __name__ == '__main__':
    rectangle = Rectangle(5, 5)
    square = Square(5)
    assert get_squashed_height_area(rectangle) == 5  # expected 5
    assert get_squashed_height_area(square) == 5  # expected 5

>>>
Traceback (most recent call last):
  File "F:/python基础/面向对象/test.py", line 76, in <module>
    assert get_squashed_height_area(square) == 5  # expected 5
AssertionError

当我们在软件组件 get_squashed_height_area 中重置宽度时,就产生了问题——长方形能实现只改变长或宽,而正方形长和宽都改变了,从而导致后面的断言错误。


正是因为正方形的定义要求使得其内部实现与长方形的内部实现有所不同,导致在软件组件中使用时,前者不能实现与对后者的期望,即不能在软件中替换父类长方形而不出现程序错误,这违背了里氏替换原则。


所以,正方形就不应该是长方形,不能产生 is-a 关系,即正方形不能继承长方形。


正确的做法应该是先实现一个抽象的四边形类,然后再通过继承的方式分别实现长方形与正方形。


再次强调,里氏替换原则旨在要求实现高复用的、正确的继承关系。


个人认为它应该是面向对象设计原则 SOLID 中最重要的一项。


设计模式-软件设计原则-里氏代换原则
SOLID Design Principles Explained: The Liskov Substitution Principle with Code Examples
Python Liskov Substitution Principle

面向对象编程(三):里氏替换原则

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/570268.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-09
下一篇 2022-04-09

发表评论

登录后才能评论

评论列表(0条)

保存