Skip to content

面向对象三大特性

Python面向对象三大特性:

  • 封装 Encapsulation
  • 继承 Inheritance
  • 多态 Polymorphism

1.封装

封装是使用特殊的语法,对成员属性和成员方法进行包装,限制一些访问和操作,达到保护和隐藏的目的。

封装机制保证了类内部数据结构的完整性,因为使用类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。

对一个类实现良好的封装,用户只能借助暴露出来的类方法来访问数据,可以在这些暴露的方法中加入适当的控制逻辑,即可控制用户对类中属性或方法的操作。

对类进行良好的封装,主要是内部使用封装的成员,也提高了代码的复用性。

类成员封装的级别如下,对应方法也是类似处理。

  • 公有的(public)
  • 保护的(protected),在Python中并没有实现protected封装,属于开发者的约定俗成。
  • 私有的(private),在Python中private封装是通过改名策略来实现的,并不是真正的私有化。
访问限制 共有的public 受保护的protected 私有的private
在类的内部 OK OK OK
在类的外部 OK No (Python中可以) No

如下例:

  • name是共有属性,可以在外部调用tom.name
  • _age是受保护属性,理论上在外部是不可调用的,但在Python中是可以调用的tom._age
  • __phone是私有属性,在外部是不可调用的,tom.__get_phone()报错“属性不存在”。
  • 在类的内部对受保护对象和私有对象没有访问限制。_get_age可以调用私有属性__phone
class Person():
    name = 'name'  # 共有属性 public
    _age = 0  # 受保护属性 protected
    __phone = 'phone'  # 私有属性 private

    def __init__(self, n, a, p):
        self.name = n
        self._age = a
        self.__phone = p

    def get_name(self):
        print(f'My name is {self.name}')

    def _get_age(self):
        print(f'My age is {self._age}') # 受保护属性,Python中是可以调用
        print(f'My age is {self.__phone}') # 可以内部调用私有属性__phone

    def __get_phone(self): # 私有属性,外部不可调用
        print(f'My phone is {self.__phone}')


tom = Person('Tom', 18, 12345678)

print(tom.name)
# 'Tom'
print(tom._age)
# 18
print(tom.__phone)
# AttributeError: 'Person' object has no attribute '__phone'
print(tom._Person__phone)
# 12345678
tom.get_name()
# My name is Tom

tom._get_age()
# My age is 18
# My age is 12345678

tom.__get_phone()
# AttributeError: 'Person' object has no attribute '__get_phone'
tom._Person__get_phone()
# My phone is 12345678

如上例中私有属性__phone,在Python中如果使用双下画线__作为前缀,Python会对其进行 名称改编(name mangling) ,以防止外部直接访问。名称改编的规则是将属性名改为_ClassName__AttributeName的形式。上述代码中,虽然直接访问tom.__phone会引发AttributeError,但可以通过名称改编后的名字来访问print(tom._Person__phone)

上例中的私有方法__get_phone,Python也会对其进行名称改编,外部不能直接访问,但也是可以通过名称改编后的名字来访问tom._Person__get_phone(),但不推荐这样的做法,因为它违反了封装的原则。在实际开发中,应该遵循类的设计意图,通过提供的公共接口(public methods)来访问和操作对象的状态,而不是直接访问私有或受保护的成员。这样做可以保持代码的封装性和可维护性。

1.2.property()函数

如前面提到的,在类的内部对受保护对象(如_age)和私有对象(如__phone)没有访问限制,所以,把所有属性私有化,通过方法访问它们,在Python中是不成立的,因为在Python中并没有真正的私有成员的概念。因此,在Python中推荐如下的写法:

class Person:
    def __init__(self, n, a, p):
        self.name = n
        self.age = a
        self.phone = p

tom = Person("Tom", 18, 12345678)
print(tom.name)  # 输出:Tom
print(tom.age)  # 输出:18
print(tom.phone)  # 输出:12345678

Python提供了一个property()函数,用于将方法变得看起来像属性。因此可以用直接成员访问来编写代码,如果我们偶尔需要改变取/赋属性值时的代码逻辑,也可以不用更改接口就能做到。

看下面精简的代码示例。

class Person:
    def __init__(self, name: str) -> None:
        self._name = name
        self._age: int

    def _get_name(self):
        return self._name

    def _set_name(self, name: str) -> None:
        self._name = name


p = Person("Tom")
print(p._name)  # Tom

修改上面代码,最下方增加property声明,它为Person类创建了一个新的虚拟属性,名为age。 当访问age的时候,会调用_get_age()方法;当更改age值的时候,会调用_set_age()方法。 property()构造方法还可以接收另外两个参数,一个delete()函数和该属性的文档字符串。 注意,age属性有类型提示int但没有初始值,所以它可以被删掉。

class Person:
    def __init__(self, name: str) -> None:
        self._name = name
        self._age: int

    def _get_age(self):
        return self._age

    def _set_age(self, age: int) -> None:
        self._age = age

    def _del_age(self):
        print(f"Deleting age: {self._age}")
        del self._age

    age = property(_get_age, _set_age, _del_age, "this is age property")


p = Person("Tom")
p.age = 30
print(p.age)  # 30
p.age = "age property"
print(p.age)  # age property
del p.age  # Deleting age: age property

用装饰器改写上面的代码。通过@property来把age()声明为属性,这样子类可以选择使用简单的变量或者方法来实现这个属性。

class Person:
    def __init__(self, name: str) -> None:
        self._name = name
        self._age: int

    @property
    def age(self):
        """this is age property"""
        return self._age

    @age.setter
    def age(self, age: int) -> None:
        self._age = age

    @age.deleter
    def age(self):
        print(f"Deleting age: {self._age}")
        del self._age

p = Person("Tom")
p.age = 30
print(p.age)  # 30
p.age = "age property"
print(p.age)  # age property
del p.age  # Deleting age: age property

使用内置property会让行为和数据之间的界限变得模糊,有时候可能会让人疑惑,不知道该选用哪个:属性、方法或property。从理论上说,Python中的数据、property和方法都属于类的属性。虽然方法是可调用的,但它仍然是属性的一种。方法只是可调用的属性,而property是可定制的属性,建议遵循以下原则:

  • 方法应该代表的是动作,可以在对象上执行或由对象执行的东西。当你调用方法时,即使只用一个参数,它都应该做点儿什么。方法的名称通常都是动词。
  • 使用属性或property代表对象的状态。它们是用于描述对象的名词、形容词和介词。
    • 默认情况下,使用普通的属性(非property)​。它们在__init__()函数中被初始化,一开始就有值。
    • 当取值或赋值需要做额外运算时,使用property,比如数据验证、打印日志和访问控制。

2.继承

在不指定继承的父类时,所有类都继承object类(系统提供)。

  • 被其它类继承的类,称为父类,或者基类,或者超类。
  • 继承其它类的类,称为子类,或者派生类(derived class)。
  • 子类继承父类后,就拥有了父类中的所有成员(除了私有成员)。
  • 子类继承父类后,并不会把父类的成员复制给子类,而是引用。
  • 子类可以直接调用父类的方法super().BaseClassName。如果父类方法有参数要求,子类调用时也有参数要求。
  • 子类继承父类后,可以重新定义父类中的方法,称为 重写(Override)
  • 子类继承父类后,定义父类中没有的方法,被称为对父类的 扩展(Extension)
  • 一个父类可以被多个子类继承。

子类/派生类(derived class) 定义的语法如下所示:

class BaseClassName():
    <statement-1>
    .
    .
    .
    <statement-N>

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

名称BaseClassName必须定义于包含派生类定义的作用域中。 也允许用其他任意表达式代替基类名称所在的位置,例如,当基类定义在另一个模块中的时候:

class DerivedClassName(modname.BaseClassName):

派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地(recursively)应用。

派生类的实例化没有任何特殊之处: DerivedClassName()会创建该类的一个 新实例 。 方法引用将按以下方式解析:搜索相应的类属性,如有必要将按基类继承链逐步向下查找,如果产生了一个函数对象则方法引用就生效。

派生类可能会重写(override)其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,所以调用同一基类中定义的另一方法的基类方法最终可能会调用覆盖它的派生类的方法。

在派生类中的重载方法(overriding method)实际上可能想要扩展而非简单地替换同名的基类方法。 有一种方式可以简单地直接调用基类方法:即调用BaseClassName.methodname(self, arguments)。 请注意,仅当此基类可在全局作用域中以BaseClassName的名称被访问时方可使用此方式。

Python有两个内置函数可被用于继承机制:

  • 使用isinstance()来检查一个实例的类型: isinstance(obj, int)仅会在obj.__class__int或某个派生自int的类时为True
  • 使用issubclass()来检查类的继承关系:issubclass(bool, int)True,因为boolint的子类。 但是,issubclass(float, int)False,因为float不是int的子类。

基于上面的Persons类定义,我们可以创建一个派生类Student,该类继承自Person类,并扩展其功能。同时,我们将在Student类中重载(override)一个方法,并在该方法中调用基类(父类)的同名方法,以展示如何扩展而非简单替换父类方法。此外,我们将使用isinstance()和issubclass()来检查实例的类型和类的继承关系。 定义派生类Student

完整代码如下:

class Person:
    name = "name"  # 共有属性 public
    _age = 0  # 受保护属性 protected
    __phone = "phone"  # 私有属性 private

    def __init__(self, n, a, p):
        self.name = n
        self._age = a
        self.__phone = p

    def get_name(self):
        print(f"My name is {self.name}")

    def _get_age(self):
        print(f"My age is {self._age}")  # 受保护属性,Python中是可以调用
        print(f"My age is {self.__phone}")  # 可以内部调用私有属性__phone

    def __get_phone(self):  # 私有属性,外部不可调用
        print(f"My phone is {self.__phone}")


class Student(Person):
    def __init__(self, n, a, p, major):
        super().__init__(n, a, p)  # 调用父类的构造函数
        self.major = major  # 新增属性:专业

    def get_info(self):
        # 调用父类的get_name方法
        Person.get_name(self)  # 直接调用基类方法
        print(f"My major is {self.major}")

    def __str__(self):
        return f"Student(name={self.name}, age={self._age}, phone={self._Person__phone}, major={self.major})"


tom = Person("Tom", 18, 12345678)

print(tom.name)
# 'Tom'
print(tom._age)
# 18
print(tom.__phone)
# AttributeError: 'Person' object has no attribute '__phone'
print(tom._Person__phone)
# 12345678
tom.get_name()
# My name is Tom

tom._get_age()
# My age is 18
# My age is 12345678

tom.__get_phone()
# AttributeError: 'Person' object has no attribute '__get_phone'
tom._Person__get_phone()
# My phone is 12345678

# 创建Student实例
alice = Student("Alice", 20, 987654321, "Computer Science")

# 调用重载的方法
alice.get_info()
# My name is Alice 和 My major is Computer Science

# 使用isinstance检查实例类型
print(isinstance(alice, Student))
# True
print(isinstance(alice, Person))
# True,因为Student继承自Person

# 使用issubclass检查类的继承关系
print(issubclass(Student, Person))
# True,因为Student是Person的子类
print(issubclass(Person, Student))
# False,因为Person不是Student的子类

扩展方法:

  • Student类中,我们定义了一个新的方法get_info()。这个方法首先调用了父类Personget_name()方法来输出名字,然后输出专业信息。这是实现了子类中扩展父类的功能,而不是完全替换父类的方法。

直接调用基类方法:

  • get_info()方法中,我们使用Person.get_name(self)直接调用了父类Personget_name()方法。这种方式适用于基类在全局作用域中可以被访问的情况。

使用isinstance()和issubclass():

  • isinstance(student, Student)检查student是否是Student类的实例,结果为True
  • isinstance(student, Person)检查student是否是Person类的实例,结果也为True,因为Student继承自Person
  • issubclass(Student, Person)检查Student是否是Person的子类,结果为True。
  • issubclass(Person, Student)检查Person是否是Student的子类,结果为False,因为Person是基类,而不是子类。

多重继承

单继承(single-inheritance):一个类只能继承一个父类方式。定义语句如下所示:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

多继承(Multiple Inheritance):一个类去继承多个类的方式。定义语句如下所示:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

在最简单的情况下,搜索从父类所继承属性的操作是 深度优先(depth-first)从左至右(left-to-right) 的,当层次结构中存在重叠时不会在同一个类中搜索两次。 因此,如果某一属性在DerivedClassName中未找到,则会到Base1中搜索它,然后(递归地)到Base1的基类中搜索,如果在那里未找到,再到Base2中搜索,依此类推。

真实情况更复杂;方法解析顺序会动态改变以支持对super()的协同调用。 这种方式在某些其他多重继承型语言中被称为 后续方法调用(call-next-method) ,它比 单继承(single-inheritance) 语言中的uper调用更强大。

动态改变顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的 菱形关联(diamond relationships) (即至少有一个父类可通过多条路径被最底层类所访问)。 例如,所有类都是继承自object,因此任何多重继承的情况都提供了一条以上的路径可以通向 object

为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化, 保留每个类所指定的从左至右的顺序,只调用每个父类一次,并且保持 单调(monotonic) (即一个类可以被子类化而不影响其父类的优先顺序)。

看下面例子,定义了3个类和继承关系。

class F():

    def drink(self):
        print("Drink Beer")


class M():

    def drink(self):
        print("Drink Red Wine")


class C(F, M):

    def drink(self):
        print("Drink Water")

执行结果是

c = C()
c.drink()
# Drink Water

方法1:按照mro进行继承查找。

如果把C类改写为如下,可以调用父类,参照C类的mro进行,mro里面类F的上一级是类M,所以类F中的super()就是指类M

class C(F, M):

    def drink(self):
        super().drink()
        print("Drink Water")


c = C()
c.drink()
# Drink Beer
# Drink Water

C.mro()
# [<class '__main__.C'>, <class '__main__.F'>, <class '__main__.M'>, <class 'object'>]

方法2:“指名道姓”调用。如果把C类改写为如下,可以调用M类。

class C(F, M):

    def drink(self):
        M.drink(self)
        print("Drink Water")


c = C()
c.drink()
# Drink Red Wine
# Drink Water

菱形继承和继承关系检测

菱形继承 是指:类A作为基类(这里基类是指非object类),类B和类C同时继承类A,然后类D又继承类B和类C,如下图,看起来像个钻石的形状。

    A
   / \
  B   C
   \ /
    D

在这种结构中,在调用顺序上就会出现疑惑,调用顺序究竟是以下哪一种顺序呢?

  • D->B->A->C(深度优先)
  • D->B->C->A(广度优先)

看下面代码,在Python3中,菱形 的多继承关系是按照 D->B->C->A **广度优先**的搜索方式。

class A():
    pass


class B(A):

    def test(self):
        print("init B.test()")


class C(A):

    def test(self):
        print("init C.test()")


class D(B, C):
    pass


d = D()
d.test()
# init B.test()


D.mro()
# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

对于下面这种 非菱形 的多继承关系,查找顺序是 A->B->E->C->F->D 深度优先 的搜索方式。

E     F
|     |
B(E) C(F)  D
|     |    |
 \    |   /
  \   |  /
   A(B,C,D)

代码实现:

class D():

    def test(self):
        print("init D.test()")


class F():

    def test(self):
        print("init F.test()")


class C(F):
    pass


class E():
    pass


class B(E):
    pass


class A(B, C, D):
    pass


a = A()
a.test()
# init F.test()

A.mro()
# [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>]

总结:

  1. 继承结构要尽量简单,不要过于复杂。
  2. 推荐使用minxins机制,在多继承背景下,满足继承的什么是什么的关系(is-a)

多继承关系的minxins机制

看下面例子,在Vehicle类中定义了fly的方法,会导致Car(Vehicle)的继承关系出现矛盾,汽车并不会飞,但按照上述继承关系,汽车也能飞了。这种设计违反了 单一职责原则 ,因为 Vehicle 类既定义了交通工具的通用行为,又定义了飞行功能。但是如果民航飞机和直升机都各自写自己的飞行fly方法,又违背了代码尽可能重用的原则。

class Vehicle:
    def __init__(self, name):
        self.name = name

    def move(self):
        print(f"{self.name} is moving")

    def fly(self):
        """
        飞行功能相应的代码
        """
        print(f"{self.name} is flying")


# 民航飞机
class CivilAircraft(Vehicle):
    def __init__(self, name):
        super().__init__(name)


# 直升飞机
class Helicopter(Vehicle):
    def __init__(self, name):
        super().__init__(name)


# 汽车
class Car(Vehicle):
    def __init__(self, name):
        super().__init__(name)


if __name__ == "__main__":
    car = Car("Toyota")
    car.move()
    car.fly() 

# Toyota is moving
# Toyota is flying

Python中没有类似Java接口interface的功能,但提供了Mixins机制。

  • Python对于Mixin类的命名方式一般以 Mixin, able, ible为后缀。
  • Mixin类必须功能单一,如果有多个功能,那就写多个Mixin类。
  • 一个类可以继承多个Mixin类,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类
  • Mixin类不依赖于子类的实现。
  • 子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。

我们定义的Mixin类越多,子类的代码可读性就会越差。

# 交通工具
class Vehicle:
    def __init__(self, name):
        self.name = name

    def move(self):
        print(f"{self.name} is moving")


# 为当前类混入一些功能,不是一个单纯的类
class FlyableMixin:

    def fly(self):
        """
        飞行功能相应的代码
        """
        print(f"{self.name} is flying")


# 民航飞机
class CivilAircraft(FlyableMixin, Vehicle):
    def __init__(self, name):
        super().__init__(name)


# 直升飞机
class Helicopter(FlyableMixin, Vehicle):
    def __init__(self, name):
        super().__init__(name)


# 汽车
class Car(Vehicle):
    def __init__(self, name):
        super().__init__(name)


if __name__ == "__main__":
    # 创建民航飞机实例并调用飞行功能
    civil_aircraft = CivilAircraft("Boeing 747")
    civil_aircraft.move()  # Boeing 747 is moving
    civil_aircraft.fly()  # Boeing 747 is flying

    # 创建直升飞机实例并调用飞行功能
    helicopter = Helicopter("Bell 407")
    helicopter.move()  # Bell 407 is moving
    helicopter.fly()  # Bell 407 is flying

    # 创建汽车实例并尝试调用飞行功能
    car = Car("Toyota")
    car.move()  # Toyota is moving
    try:
        car.fly()
    except AttributeError as e:
        print(f"Error: {e}")  # Error: 'Car' object has no attribute 'fly'

组合与聚合

在一个类中以另一个类的对象作为数据属性,称为类的 组合(Composition)。组合与继承都是用来解决代码的重用性问题。

  • 继承体现“是”的关系,当类之间有很多相同之处,用继承。
  • 组合体现“有”的关系,当类之间有显著不同,一个类是另一个类的属性是,用组合。

下例是计算圆环的面积和周长,圆环是由两个圆组成的,圆环的面积是外面圆的面积减去内部圆的面积。圆环的周长是内部圆的周长加上外部圆的周长。

这个例子演示了类ring里面的属性circle1circle2正是另一个类Circle

from math import pi

class Circle():

    def __init__(self, r):
        self.r = r

    def area(self):
        return pi * self.r * self.r

    def perimeter(self):
        return 2 * pi * self.r


class Ring():

    def __init__(self, r1, r2):
        self.circle1 = Circle(r1)
        self.circle2 = Circle(r2)

    def area(self):
        return abs(self.circle1.area() - self.circle2.area())

    def permiter(self):
        return self.circle1.perimeter() + self.circle2.perimeter()


ring = Ring(5, 8)

print(ring.area())
# 122.52211349000193

print(ring.permiter())
# 81.68140899333463

下面的例子演示了如何通过传参的方式进行类的组合。

class Birthday():

    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day


class Course():

    def __init__(self, course_name, course_period):
        self.course_name = course_name
        self.course_period = course_period


class Professor():

    def __init__(self, name, gender, birth, course):
        self.name = name
        self.gender = gender
        self.birth = birth # 通过self.birth访问Birthday实例的birth属性
        self.course = course # 通过self.course访问Course实例的course属性

    def teach(self):
        print(f"
              Professor name: {self.name}; 
              Gender: {self.gender}; 
              Birthday: {self.birth.year}-{self.birth.month}-{self.birth.day}, 
              Course name: {self.course.course_name} 
              and period: {self.course.course_period}
              "
              )


prof = Professor('Tom', 'Male', Birthday(1985, 5, 5), Course('Chinese', '2022/3/1 ~ 2022/6/30'))

prof.teach()
# Professor name: Tom; Gender: Male; Birthday: 1985-5-5, Course name: Chinese and period: 2022/3/1 ~ 2022/6/30

下面2个例子是属于 聚合(Aggregation) 。组合关系是聚合关系,因为聚合是一种更广义的组合。任何组合关系一定也是聚合关系,但聚合关系不一定是组合关系。

例:Car类包含Engine类的实例,因为Car有一个Engine。在Car类定义中通过self.engine = engine来实现访问Engine实例的方法。

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        print(f"Engine with {self.horsepower} horsepower is starting")

class Car:
    def __init__(self, make, model, engine):
        self.make = make
        self.model = model
        self.engine = engine  # Car 有一个 Engine 实例

    def drive(self):
        print(f"Driving {self.make} {self.model}")
        self.engine.start()  # 调用 Engine 实例的 start 方法

# 创建 Engine 实例
engine = Engine(300)

# 创建 Car 实例,传递 Engine 实例作为属性
car = Car("Toyota", "Corolla", engine)

# 调用 Car 实例的 drive 方法
car.drive()
# Driving Toyota Corolla
# Engine with 300 horsepower is starting

例:假设我们有一个Book类和一个Library类。Library类包含多个Book类的实例,因为Library有多个Book

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def display(self):
        print(f"Book: {self.title} by {self.author}")

class Library:
    def __init__(self):
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def list_books(self):
        for book in self.books:
            book.display()

# 创建 Book 实例
book1 = Book("1984", "George Orwell")
book2 = Book("To Kill a Mockingbird", "Harper Lee")

# 创建 Library 实例
library = Library()

# 向 Library 添加 Book 实例
library.add_book(book1)
library.add_book(book2)

# 列出 Library 中的所有 Book
library.list_books()
# Book: 1984 by George Orwell
# Book: To Kill a Mockingbird by Harper Lee

聚合 vs 组合的对比

特性 组合(Composition) 聚合(Aggregation)
生命周期管理 部分对象由整体对象创建并销毁。 部分对象独立于整体对象存在。
依赖关系 强耦合,部分对象不能独立存在。 松耦合,部分对象可以独立存在。
代码实现 整体对象在内部创建部分对象(如构造函数)。 整体对象通过参数接收部分对象(如方法传参)。
典型场景 汽车与引擎、树与树叶。 教室与学生、公司与员工。

总结:

  • 组合:整体对象完全控制部分对象的生命周期,关系紧密(“包含”关系)。
  • 聚合:部分对象可以独立存在,关系松散(“使用”关系)。
  • 选择依据:根据对象之间的依赖关系和生命周期需求决定使用组合还是聚合。

3.多态

多态意味着相同的函数名用于不同的情形。 如下例,len()被用于不同的情形。

# len() being used for a string
print(len("geeks"))
# 5

# len() being used for a list
print(len([10, 20, 30]))
# 3

3.1.类方法的多态性

下面的代码展示了 Python 如何以相同的方式使用两种不同的类类型。

多态性:我们创建了一个遍历对象元组的 for 循环。然后调用方法而不用关心每个对象是哪个类类型。我们假设这些方法实际上存在于每个类中。尽管IndiaUSA类是独立定义的,并且没有显式地继承自同一个基类,但它们都定义了相同名称的方法:capitallanguagetype。这些方法的签名(名称和参数列表)是相同的,这使得它们可以通过相同的接口被调用。

迭代:使用for循环迭代一个包含两个实例的元组for country in (obj_ind, obj_usa)。在每次迭代中,country变量分别指向obj_indobj_usa

动态方法调用:在循环体内,通过country.capital()country.language()country.type()动态调用每个实例的方法。Python在运行时根据country变量的实际类型(IndiaUSA)调用相应的方法。

class India():
    # Method to print the capital of India
    def capital(self):
        print("New Delhi is the capital of India.")

    # Method to print the most widely spoken language in India
    def language(self):
        print("Hindi is the most widely spoken language of India.")

    # Method to print the type of country India is
    def type(self):
        print("India is a developing country.")


class USA():
    # Method to print the capital of USA
    def capital(self):
        print("Washington, D.C. is the capital of USA.")

    # Method to print the primary language of USA
    def language(self):
        print("English is the primary language of USA.")

    # Method to print the type of country USA is
    def type(self):
        print("USA is a developed country.")


# Create an instance of India
obj_ind = India()
# Create an instance of USA
obj_usa = USA()

# Iterate over the instances and call their methods
for country in (obj_ind, obj_usa):
    country.capital()
    country.language()
    country.type()

# New Delhi is the capital of India.
# Hindi is the most widely spoken language of India.
# India is a developing country.
# Washington, D.C. is the capital of USA.
# English is the primary language of USA.
# USA is a developed country.

3.2.继承的多态性

在 Python 中,多态允许我们在子类中定义与父类中的方法同名的方法。 在继承中,子类继承父类的方法。 但是,可以修改从父类继承的子类中的方法。 这在从父类继承的方法不太适合子类的情况下特别有用。 在这种情况下,我们在子类中重新实现该方法。 这种在子类中重新实现方法的过程称为 方法覆盖(Method Overriding)

在下面的例子中:

  • sparrow类和ostrich类继承了Bird类的所有属性和方法。
  • sparrow类和ostrich类重写了flight方法,提供了特定于麻雀和鸵鸟的飞行能力信息。
class Bird:

    def intro(self):
        print("There are many types of birds.")

    def flight(self):
        print("Most of the birds can fly but some cannot.")


class sparrow(Bird):

    def flight(self):
        print("Sparrows can fly.")


class ostrich(Bird):

    def flight(self):
        print("Ostriches cannot fly.")


obj_bird = Bird()
obj_spr = sparrow()
obj_ost = ostrich()

obj_bird.intro()
# There are many types of birds.

obj_bird.flight()
# Most of the birds can fly but some cannot.

obj_spr.intro()
# There are many types of birds.

obj_spr.flight()
# Sparrows can fly.

obj_ost.intro()
# There are many types of birds.

obj_ost.flight()
# Ostriches cannot fly.

方法覆盖(Method Overriding)是指子类重新定义父类中已有的方法,以改变或扩展该方法的行为。覆盖后的方法在子类中优先于父类中的方法被调用。

特点:

  • 继承关系:方法覆盖发生在子类和父类之间。
  • 方法签名相同:子类中的方法与父类中的方法具有相同的名称和参数列表。
  • 动态绑定:在运行时根据对象的实际类型决定调用哪个方法。

在上面的代码中,sparrowostrich 类都覆盖了父类 Birdflight 方法,分别实现了不同的行为。当调用 flight 方法时,实际执行的是子类中覆盖后的方法。

方法重载(Method Overloading)是指在一个类中定义多个同名方法,但这些方法的参数列表不同(参数类型或参数数量不同)。

特点:

  • 同一个类中:方法重载发生在同一个类中。
  • 方法名相同:重载的方法名称相同。
  • 参数列表不同:重载的方法参数列表必须不同(参数类型或参数数量不同)。

Python 不支持传统的方法重载(如 Java 或 C++ 中的重载),但可以通过默认参数或可变参数实现类似的功能。

方式 1:使用默认参数

class Calculator:
    def add(self, a, b=0):  # 通过默认参数实现重载
        return a + b

calc = Calculator()
print(calc.add(2))      # 输出: 2
print(calc.add(2, 3))   # 输出: 5

方式 2:使用可变参数

class Calculator:
    def add(self, *args):  # 通过可变参数实现重载
        return sum(args)

calc = Calculator()
print(calc.add(2))      # 输出: 2
print(calc.add(2, 3))   # 输出: 5
print(calc.add(2, 3, 4))# 输出: 9

方法覆盖vs方法重载

特性 方法覆盖Overriding 方法重载Overloading
定义位置 子类中重新定义父类的方法 同一个类中定义多个同名方法
方法签名 方法名和参数列表必须相同 方法名相同,但参数列表必须不同
调用时机 运行时根据对象类型决定调用哪个方法 编译时根据参数列表决定调用哪个方法
Python支持 支持 不支持(但可以通过默认参数或可变参数实现)

3.3.函数和对象的多态性

我们也可以创建一个可以接受任何对象的函数,允许多态性。

在下面例子中,我们创建一个名为func()的函数,传入参数是obj的对象。 在这种情况下,我们调用三个方法,即capital()language()type(),每个方法都定义在IndiaUSA两个类中。 我们可以使用相同的 func() 函数调用它们的动作:

class India():

    def capital(self):
        print("New Delhi is the capital of India.")

    def language(self):
        print("Hindi is the most widely spoken language of India.")

    def type(self):
        print("India is a developing country.")


class USA():

    def capital(self):
        print("Washington, D.C. is the capital of USA.")

    def language(self):
        print("English is the primary language of USA.")

    def type(self):
        print("USA is a developed country.")


def func(obj):
    obj.capital()
    obj.language()
    obj.type()


obj_ind = India()
obj_usa = USA()

func(obj_ind)
# New Delhi is the capital of India.
# Hindi is the most widely spoken language of India.
# India is a developing country.

func(obj_usa)
# Washington, D.C. is the capital of USA.
# English is the primary language of USA.
# USA is a developed country.

3.4.鸭子类型和白鹅类型

在Python中实现多态主要有两种机制:白鹅类型(Goose Typing)和鸭子类型(Ducking Typing)。白鹅类型和鸭子类型不仅是两种机制,也是两种不同的编程风格。

下面是一个打印商品价格的例子,分别用鸭子类型和白鹅类型实现。

鸭子类型(Duck Typing)是Python中的一种动态类型检查机制,它强调对象的行为(即方法和属性)而不是对象的类型。只要对象具有所需的方法或属性,就可以在特定的上下文中使用,而不需要显式地继承自某个特定的类。

在下面的示例代码中,FoodClothesCoffee类都定义了一个price_info方法,虽然这些类之间没有显式的继承关系,但它们都可以被放入一个列表中,并在循环中调用price_info方法。这展示了鸭子类型的特点:只要对象具有price_info方法,就可以被调用,而不需要这些对象继承自同一个基类。

class Food:
    price = 4

    def price_info(self):
        print(f"{self.__class__.__name__} price: ${self.price}")


class Clothes:
    price = 5

    def price_info(self):
        print(f"{self.__class__.__name__} price: ${self.price}")


class Coffee:
    price = 6

    def price_info(self):
        print(f"{self.__class__.__name__} price: ${self.price}")


def print_price(item):
    item.price_info()


if __name__ == "__main__":
    goods = [Food(), Clothes(), Coffee()]
    for good in goods:
        print_price(good)

# Food price: $4
# Clothes price: $5
# Coffee price: $6

白鹅类型。

Python中的白鹅类型机制就是强类型语言中实现多态的标准模式,即通过调取父类的虚函数或者继承的函数来完成不同的行为。

在白鹅类型中,直接让所有对象的类继承父类PricedItem中的抽象方法price_info

定义抽象基类PricedItem

  • 使用ABC(Abstract Base Class)作为基类。
  • 使用@abstractmethod装饰器定义抽象方法price_info。这意味着任何继承自PricedItem的类都必须实现price_info方法。

定义具体类:

  • FoodClothesCoffee类继承自PricedItem,并实现了price_info方法。
  • 每个类都有一个类属性price,用于存储价格信息。

定义print_price函数:

  • print_price函数接受一个PricedItem类型的参数,并调用其price_info方法。
  • 通过类型注解item: PricedItem,明确指出item参数必须是PricedItem或其子类的实例。
from abc import ABC, abstractmethod


class PricedItem(ABC):
    @abstractmethod
    def price_info(self):
        pass


class Food(PricedItem):
    price = 4

    def price_info(self):
        print(f"{self.__class__.__name__} price: ${self.price}")


class Clothes(PricedItem):
    price = 5

    def price_info(self):
        print(f"{self.__class__.__name__} price: ${self.price}")


class Coffee(PricedItem):
    price = 6

    def price_info(self):
        print(f"{self.__class__.__name__} price: ${self.price}")


def print_price(item: PricedItem):
    item.price_info()


if __name__ == "__main__":
    goods = [Food(), Clothes(), Coffee()]
    for good in goods:
        print_price(good)

# Food price: $4
# Clothes price: $5
# Coffee price: $6

抽象基类和具体类的区别:

@abstractmethod 装饰器不能直接放在具体类(如 FoodClothesCoffee)中使用。@abstractmethod 通常用于抽象基类(Abstract Base Class, ABC)中,用于定义必须由子类实现的抽象方法。具体类(即非抽象类)不能包含抽象方法,因为具体类的目的是提供完整的实现,而不是定义需要被进一步实现的接口。

抽象基类(ABC):

  • 抽象基类是一个不能被直接实例化的类,它定义了一些抽象方法,这些方法必须由子类实现。
  • 抽象方法使用 @abstractmethod 装饰器标记,表示这些方法没有实现,子类必须提供具体的实现。

具体类:

  • 具体类是可以被直接实例化的类,它提供了所有方法的具体实现。
  • 具体类不能包含抽象方法,因为抽象方法没有具体的实现,而具体类的目的是提供完整的功能。

3.5.类方法和静态方法

类方法(Class method)也叫绑定方法,必须把类作为传入参数,使用cls作为第一个传入参数,而静态方法(Static Method),也叫非绑定方法,不需要特定的参数。

类方法是绑定到类的,不是绑定到类对象,所以类方法可以访问或修改类,并对所有类实例生效。

静态方法无法直接访问或修改类,因为静态方法是不知道类本身的,静态方法是属于工具类方法,基于传入的参数完成特定的功能,其实就是一个普通函数而已。

Python中使用@classmethod装饰器(decorator)来创建一个类方法,用@staticmethod装饰器来创建一个静态方法。

语法格式:

@classmethod
def fun(cls, arg1, arg2, ...):

其中:

fun: 需要转换成类方法的函数
returns: 函数的类方法

classmethod()方法绑定到类而不是对象。类方法可以被类和对象调用。这些方法可以通过类或对象进行调用。

例1:创建一个简单的classmethod

创建一个类Training,有类变量course和方法purchase

我们通过把函数Training.purchase传给classmethod(),把该方法转成类方法,然后直接调用它,而无需先创建对象。

可以看出转换前后Training.purchase的类型变化,purchase方法从普通方法变成了一个类方法。

调用 Training.purchase() 时,purchase 方法作为类方法被调用。类方法的第一个参数 cls 自动绑定到类 Training,因此 cls.course 访问的是类属性 course

class Training:
    # 类属性
    course = "Python for Data Analysis"

    # 普通方法,接受一个参数obj,obj用于访问类属性course
    def purchase(obj):
        print("Puchase course : ", obj.course)

# 打印 purchase 方法的类型
print(type(Training.purchase))
# <class 'function'>

# 将 purchase 方法转换为类方法
Training.purchase = classmethod(Training.purchase)

# 调用类方法
Training.purchase()
# Puchase course :  Python for Data Analysis

# 再次打印 purchase 方法的类型
print(type(Training.purchase))
# <class 'method'>

例2:使用装饰器@classmethod创建工厂类。

class Training:
    def __init__(self, course):
        self.course = course

    @classmethod
    def purchase(cls, course): # 类方法
        return cls(course)

    def display(self):
        print('Purchase course: ', self.course)

# 创建实例
training = Training("Python for Data Analysis")
# 调用实例方法
training.display()
# 输出: Purchase course:  Python for Data Analysis

对比例1和例2,有下面这些不同。

  1. 实例属性 vs 类属性:
    • 例1中使用类属性 course,所有实例共享同一个类属性。
    • 例2中使用实例属性 course,每个实例有自己的 course 属性。
  2. 工厂方法的使用:
    • 例1中类方法 purchase 用于打印类属性,没有创建新实例。
    • 例2中类方法 purchase 作为工厂方法,用于创建并返回类的新实例。
  3. 实例方法的使用:
    • 例1中没有定义实例方法。
    • 例2中定义了实例方法 display,用于打印实例属性。

例3:使用staticmethod()classmethod()

修改上面的 Training 类代码来举例说明静态方法(staticmethod)和类方法(classmethod)的使用。

静态方法不接受类或实例作为第一个参数,而类方法接受类作为第一个参数。

class Training:
    def __init__(self, course):
        self.course = course

    @classmethod
    def purchase(cls, course):
        # 类方法,用于创建并返回类的新实例
        return cls(course)

    def display(self):
        # 实例方法,用于打印实例属性
        print('Purchase course: ', self.course)

    @staticmethod
    def get_course_info():
        # 静态方法,用于返回课程信息
        return "This is a data analysis course."

    @classmethod
    def update_course_name(cls, new_name):
        # 类方法,用于更新课程名称
        print(f"Updating course name to: {new_name}")
        # 假设这里有一个类属性 course_name,我们更新它
        cls.course_name = new_name

# 创建实例
training = Training("Python for Data Analysis")

# 调用实例方法
training.display()
# 输出: Purchase course:  Python for Data Analysis

# 调用静态方法
info = Training.get_course_info()
print(info)
# 输出: This is a data analysis course.

# 调用类方法作为工厂方法
new_training = Training.purchase("Advanced Python for Data Science")
new_training.display()
# 输出: Purchase course:  Advanced Python for Data Science

# 调用类方法更新课程名称
Training.update_course_name("Python for Machine Learning")
# 输出: Updating course name to: Python for Machine Learning

解释:

  1. 静态方法 get_course_info
    • 使用 @staticmethod 装饰器定义。
    • 静态方法不接受类或实例作为第一个参数,因此可以不依赖于类或实例的状态。
    • 静态方法通常用于提供与类相关的功能,但不涉及类或实例的属性。
  2. 类方法 update_course_name
    • 使用 @classmethod 装饰器定义。
    • 类方法的第一个参数是类本身,通常命名为 cls
    • 类方法可以访问和修改类属性,这些修改对所有实例都有效。
    • 在例3中,update_course_name 方法用于更新类属性 course_name

小结:

若类中需要一个功能,该功能的实现代码中需要引用对象,则将其定义成对象方法/实例方法(如display);需要引用类,则将其定义成类方法(如purchase);无需引用类或对象,则将其定义成静态方法(如get_course_info)。

下面再解释一下:当我们需要创建类的实例,如何使用类方法作为工厂方法,根据不同的参数或条件创建不同类型的实例。

  1. 工厂方法的概念:
    • 工厂方法是一种设计模式,用于创建对象,但允许用户决定要实例化的类。它提供了一个统一的接口来创建对象,而不需要直接调用类的构造函数。
    • 工厂方法的主要优点是可以在不改变客户端代码的情况下,轻松地添加新的类或修改创建逻辑。
  2. 类方法作为工厂方法:
    • 类方法(@classmethod)可以接受类本身作为第一个参数(通常命名为cls),并调用类的构造函数来创建实例。
    • 通过类方法,可以在创建实例之前进行一些预处理或条件判断,然后根据这些条件调用不同的构造函数或返回不同类型的实例。

假设Training类中,我们希望根据不同的课程类型能创建不同的实例。下面的代码使用类方法作为工厂方法来实现这一点。

class Training:
    def __init__(self, course):
        self.course = course

    @classmethod
    def purchase(cls, course):
        # 根据课程类型创建不同类型的实例
        if course.lower() == "python for data analysis":
            return cls(course)
        elif course.lower() == "python for machine learning":
            return cls(course)
        else:
            raise ValueError("Unsupported course type")

    def display(self):
        print(f'Purchase course: {self.course}')

# 使用类方法作为工厂方法
training1 = Training.purchase("Python for Data Analysis")
training1.display()
# 输出: Purchase course: Python for Data Analysis

training2 = Training.purchase("Python for Machine Learning")
training2.display()
# 输出: Purchase course: Python for Machine Learning

# 尝试创建一个不支持的课程类型
try:
    training3 = Training.purchase("Java for Beginners")
except ValueError as e:
    print(e)
# 输出: Unsupported course type

3.6.猴子补丁

猴子补丁(monkey patch)是动态为已经创建出的对象增加新的方法和属性成员的一种机制,也就是动态打补丁。

实例化对象的猴子补丁

class Test:
    def __init__(self):
       self.a = 1

    def func1(self, x, y):
       print(x + y)


# 正常实例化
test = Test()
test.func1(1, 1)
# 2

# 修改实例
test.func1 = lambda x, y : print(x + 2 * y)
test.func1(1, 1)
# 3

# 通过修改实例,访问内部成员变量。
test.func1 = lambda x, y : print(x + 2 * y + self.a)
test.func1(1, 1)
# NameError: name 'self' is not defined
test.func1 = lambda self, x, y : print(x + 2 * y + self.a)
test.func1(test, 1, 1)
# 4

类对象的猴子补丁

class Test:
    def __init__(self):
       self.a = 1

    def func1(self, x, y):
       print(x + y)


# 修改类成员,实例化后的结果已修改。
Test.func1 = lambda self, x, y : print(x + 2 * y)

test = Test()
test.func1(1, 1)
# 3


# 修改类成员,并访问成员变量,实例化后的结果已修改。
Test.func1 = lambda self, x, y : print(x + 2 * y + self.a)

test = Test()
test.func1(1, 1)
# 4

# 增加类成员。
Test.func2 = lambda self, p, q: print(p + 3 * q + self.a)
test = Test()
test.func1(1, 1)
# 4
test.func2(1, 3)
# 11

5.SOLID原则

面向对象设计的 SOLID 原则是构建可维护、可扩展、高内聚低耦合代码的核心指导准则。

5.1.单一职责原则(Single Responsibility Principle, SRP)

核心思想:一个类应该只有一个引起它变化的原因(即只负责一个功能)。

违反示例:

class Order:
    def calculate_total(self): 
        # 计算订单总价
        pass

    def print_invoice(self): 
        # 打印订单发票
        pass

    def save_to_database(self): 
        # 保存订单到数据库
        pass

问题:Order 类同时负责计算、打印和存储,职责过多。

改进:拆分为多个类:

class Order:
    def calculate_total(self): 
        pass

class InvoicePrinter:
    def print_invoice(self, order): 
        pass

class OrderRepository:
    def save_to_database(self, order): 
        pass

5.2.开闭原则(Open-Closed Principle, OCP)

核心思想:软件实体(类、模块等)应对扩展开放,对修改关闭。

违反示例:

class AreaCalculator:
    def area(self, shape):
        if isinstance(shape, Rectangle):
            return shape.width * shape.height
        elif isinstance(shape, Circle):
            return 3.14 * shape.radius ** 2

问题:每新增一种图形(如三角形),都要修改 AreaCalculator

改进:通过抽象类扩展:

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def area(self):
        return 3.14 * self.radius ** 2

# 新增三角形无需修改 AreaCalculator
class Triangle(Shape):
    def area(self):
        return 0.5 * self.base * self.height

5.3.里氏替换原则(Liskov Substitution Principle, LSP)

Liskov替换原则(Liskov Substitution Principle, LSP)是面向对象设计 SOLID 原则中的 "L",由 Barbara Liskov 于 1987 年提出。其核心思想是:
子类对象必须能够替换父类对象,且程序的行为在替换后仍然正确。简单来说,任何使用父类的地方,都可以透明地替换为子类,而不会引发错误或意外行为

LSP 要求子类必须遵守以下约束:

  1. 方法签名兼容性
    • 子类的方法参数类型、返回类型必须与父类一致或更宽松(协变返回类型允许返回子类)。
  2. 前置条件(Preconditions)不能强化
    • 子类方法对输入参数的限制不能比父类更严格。
  3. 后置条件(Postconditions)不能弱化
    • 子类方法对返回结果的保证不能比父类更宽松。
  4. 不变量(Invariants)必须保持
    • 子类必须维护父类定义的所有不变量(例如,属性取值范围)。
  5. 不抛出新的异常
    • 子类方法不能抛出父类方法未声明的检查型异常(非检查型异常如 RuntimeException 可以)。

核心思想:子类必须能够替换父类,且替换后程序行为不变。

违反 LSP 的典型例子:矩形(Rectangle)与正方形(Square)的继承问题。

问题分析:当 Square 替换 Rectangle 时,调用 set_widthset_height 会同时修改另一个属性,破坏了 Rectangle 的行为预期。例如,以下代码预期矩形面积应变为 5*4=20,但使用 Square 会得到 4*4=16

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

    def set_width(self, width):
        self.width = width

    def set_height(self, height):
        self.height = height

    def area(self):
        return self.width * self.height


class Square(Rectangle):
    def set_width(self, width):
        # 正方形强制将宽和高设为相同值
        self.width = width
        self.height = width

    def set_height(self, height):
        self.width = height
        self.height = height


def resize(shape: Rectangle):
    shape.set_width(5)
    shape.set_height(4)
    assert shape.area() == 20  # 若 shape 是 Square,此处断言失败!


if __name__ == "__main__":
    r = Rectangle(1, 2)
    resize(r)
    print("Rectangle pass")

    s = Square(1, 2)
    resize(s)  # 报错
    print("Square pass")

解决方案:避免不合理的继承关系。正方形和矩形在数学上是“is-a”关系,但在编程中继承会导致问题。应通过 组合接口分离 解决:

from abc import ABC, abstractmethod


# 定义抽象基类(接口),约束子类必须实现 set_dimensions 方法
class Shape(ABC):
    @abstractmethod
    def set_dimensions(self, *args):
        pass

    @abstractmethod
    def area(self):
        pass


# 矩形类:支持独立修改宽高
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def set_dimensions(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height


# 正方形类:通过单一参数修改边长
class Square(Shape):
    def __init__(self, side):
        self.side = side

    def set_dimensions(self, side):
        self.side = side

    def area(self):
        return self.side**2


# 修改后的 resize 函数,明确区分处理逻辑
def resize(shape_type: str, shape: Shape):
    if shape_type == "rectangle":
        shape.set_dimensions(5, 4)  # 矩形设置宽高
        assert shape.area() == 20
    elif shape_type == "square":
        shape.set_dimensions(4)  # 正方形设置边长
        assert shape.area() == 16


if __name__ == "__main__":
    # 矩形测试
    r = Rectangle(1, 2)
    resize("rectangle", r)
    print("Rectangle pass")

    # 正方形测试
    s = Square(1)
    resize("square", s)
    print("Square pass")

关键修改点:

1.引入抽象基类 Shape

  • 定义统一的接口 set_dimensions 和 area,约束所有子类必须实现这两个方法。这确保了多态性,同时不强制子类共享不兼容的行为。

2.分离 RectangleSquare,两者行为互不冲突,避免违反 LSP。

  • Rectangle:通过 set_dimensions(width, height) 独立修改宽高。
  • Square:通过 set_dimensions(side) 修改边长,保持宽高一致。

3.明确 resize 函数的行为。

  • 根据传入的形状类型(rectanglesquare),调用不同的参数设置方式,确保断言符合预期。

5.4.接口隔离原则(Interface Segregation Principle, ISP)

核心思想:客户端不应被迫依赖它们不使用的接口。

违反示例:

class MultiFunctionPrinter:
    def print_document(self):
        pass

    def scan_document(self):
        pass

    def fax_document(self):
        pass

# 普通打印机被迫实现传真功能
class BasicPrinter(MultiFunctionPrinter):
    def fax_document(self):
        raise NotImplementedError("不支持传真!")

问题:普通打印机不需要传真功能。

改进:拆分接口:

class Printer:
    def print_document(self):
        pass

class Scanner:
    def scan_document(self):
        pass

class FaxMachine:
    def fax_document(self):
        pass

class BasicPrinter(Printer):
    pass  # 只需实现打印功能

5.5.依赖倒置原则(Dependency Inversion Principle, DIP)

核心思想:高层模块不应依赖低层模块,二者都应依赖抽象。

违反示例:

class LightBulb:
    def turn_on(self):
        print("Light on")

class Switch:
    def __init__(self):
        self.bulb = LightBulb()  # 直接依赖具体类

    def operate(self):
        self.bulb.turn_on()

问题:Switch 强依赖 LightBulb,无法扩展其他设备。

改进:通过抽象解耦:

class SwitchableDevice(ABC):
    @abstractmethod
    def turn_on(self):
        pass

class LightBulb(SwitchableDevice):
    def turn_on(self):
        print("Light on")

class Fan(SwitchableDevice):
    def turn_on(self):
        print("Fan on")

class Switch:
    def __init__(self, device: SwitchableDevice):  # 依赖抽象
        self.device = device

    def operate(self):
        self.device.turn_on()

# 使用
bulb = LightBulb()
switch = Switch(bulb)
switch.operate()

5.6.总结

原则 核心目标 关键操作
SRP 职责单一 拆分大类为小类
OCP 扩展开放 用抽象/继承替代条件判断
LSP 替换安全 避免破坏父类行为
ISP 接口精简 拆分臃肿接口
DIP 依赖抽象 通过接口/抽象类解耦

SOLID 原则共同目标是 提高代码的灵活性和可维护性 。遵循这些原则,代码会更易扩展、测试和复用。