MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python类的继承与多态表现

2021-03-222.7k 阅读

Python类的继承

继承的基本概念

在Python中,继承是面向对象编程(OOP)的一个重要特性。它允许我们创建一个新类,这个新类从一个已有的类(称为基类或父类)继承属性和方法。新类被称为子类或派生类。通过继承,子类可以复用父类的代码,减少重复,同时还能根据自身需求进行扩展和修改。

假设我们有一个Animal类,它具有一些通用的属性和方法,比如namespeak方法:

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

    def speak(self):
        print(f"{self.name} makes a sound.")

现在我们想要创建一个Dog类,Dog类也是一种动物,所以它可以继承Animal类的属性和方法。我们可以这样定义Dog类:

class Dog(Animal):
    def bark(self):
        print(f"{self.name} barks.")

在上述代码中,Dog类继承自Animal类,通过括号(Animal)来表示这种继承关系。Dog类自动拥有了Animal类的__init__方法和speak方法,同时还定义了自己特有的bark方法。

访问父类的方法

在子类中,有时我们可能需要调用父类的方法,比如在子类的__init__方法中调用父类的__init__方法来初始化继承的属性。在Python 3中,我们可以使用super()函数来实现这一点。

继续以上面的AnimalDog类为例,假设Dog类有一个额外的属性breed,我们在Dog类的__init__方法中初始化这个属性,同时调用父类的__init__方法来初始化name属性:

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def bark(self):
        print(f"{self.name} of breed {self.breed} barks.")

Dog类的__init__方法中,super().__init__(name)调用了父类Animal__init__方法,并传递了name参数。这样,Dog类在初始化时,既能正确设置name属性,又能设置自己特有的breed属性。

重写父类方法

子类可以重写(override)父类的方法,也就是说,子类可以定义一个与父类方法同名的方法,从而改变该方法的行为。

例如,我们可以在Dog类中重写speak方法,使其输出更具体的信息:

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def speak(self):
        print(f"{self.name} of breed {self.breed} barks.")

    def bark(self):
        print(f"{self.name} of breed {self.breed} barks loudly.")

在这个例子中,Dog类重写了Animal类的speak方法,当我们调用Dog类实例的speak方法时,会执行Dog类中重写后的方法,而不是Animal类中的原始方法。

多重继承

Python支持多重继承,即一个子类可以从多个父类继承属性和方法。语法上,子类定义时在括号中列出多个父类,用逗号分隔。

假设我们有两个父类FlyableSwimmable,分别表示可以飞行和可以游泳的能力:

class Flyable:
    def fly(self):
        print("I can fly.")

class Swimmable:
    def swim(self):
        print("I can swim.")

现在我们创建一个Duck类,它既可以飞行又可以游泳,所以继承自FlyableSwimmable

class Duck(Flyable, Swimmable):
    def quack(self):
        print("Quack!")

Duck类的实例可以调用flyswimquack方法,因为它从FlyableSwimmable类继承了flyswim方法,同时拥有自己定义的quack方法。

然而,多重继承可能会带来一些问题,比如菱形继承问题(也称为钻石问题)。考虑以下代码:

class A:
    def method(self):
        print("Method from A")

class B(A):
    pass

class C(A):
    def method(self):
        print("Method from C")

class D(B, C):
    pass

在这个例子中,D类通过BC间接继承自A,形成了菱形结构。当D类的实例调用method方法时,Python会按照特定的顺序查找方法,这个顺序称为方法解析顺序(MRO)。在Python中,可以通过__mro__属性查看类的MRO。例如:

print(D.__mro__)

输出结果类似于:

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

这表明Python在查找D类实例的method方法时,会首先在D类中查找,如果找不到,会按照MRO的顺序依次在BCA类中查找。在这个例子中,由于C类重写了A类的method方法,并且C在MRO中排在A之前,所以D类实例调用method方法时会执行C类中的method方法。

为了避免菱形继承带来的复杂问题,在实际编程中,应谨慎使用多重继承。如果可能,尽量使用组合(将一个类的实例作为另一个类的属性)来代替多重继承,以达到类似的功能,同时保持代码的清晰和可维护性。

Python类的多态

多态的概念

多态是面向对象编程的另一个重要特性,它允许不同类的对象对同一消息(方法调用)做出不同的响应。在Python中,多态是通过继承和方法重写来实现的。

回到前面的AnimalDog类的例子,假设我们还有一个Cat类也继承自Animal类:

class Cat(Animal):
    def speak(self):
        print(f"{self.name} meows.")

现在我们有一个函数,它接受一个Animal类型的参数,并调用其speak方法:

def make_sound(animal):
    animal.speak()

我们可以传递Dog类或Cat类的实例给这个函数:

dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers")

make_sound(dog)
make_sound(cat)

输出结果为:

Buddy of breed Golden Retriever barks.
Whiskers meows.

在这个例子中,make_sound函数并不关心传递进来的对象具体是Dog类还是Cat类,它只知道这些对象都继承自Animal类,并且都有speak方法。不同类的对象对speak方法的调用做出了不同的响应,这就是多态的体现。

鸭子类型与多态

Python是一种动态类型语言,它遵循鸭子类型(Duck Typing)原则。鸭子类型的概念是:“如果它走路像鸭子,叫声像鸭子,那么它就是鸭子”。也就是说,Python并不关心对象的具体类型,只要对象具有所需的方法,就可以像预期的那样使用。

例如,我们不一定要通过继承Animal类来实现多态。假设我们有一个Robot类,它有一个make_noise方法:

class Robot:
    def make_noise(self):
        print("Beep boop!")

我们可以修改make_sound函数,使其接受任何具有make_noise方法的对象:

def make_sound(obj):
    if hasattr(obj, "make_noise"):
        obj.make_noise()
    else:
        print("Object doesn't have a make_noise method.")

然后我们可以传递Robot类的实例给这个函数:

robot = Robot()
make_sound(robot)

输出结果为:

Beep boop!

这里Robot类并没有继承自Animal类,但由于它有make_noise方法,所以可以像DogCat类一样被make_sound函数处理,这也是多态的一种体现,是基于鸭子类型的多态。

抽象基类与多态

在Python中,我们可以使用抽象基类(ABC,Abstract Base Class)来定义一组方法的规范,子类必须实现这些方法才能被视为符合该规范。这有助于确保多态行为的一致性。

Python的abc模块提供了定义抽象基类的工具。例如,我们定义一个抽象基类Shape,它有一个抽象方法area

from abc import ABC, abstractmethod

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

这里@abstractmethod装饰器将area方法标记为抽象方法,子类必须实现这个方法。现在我们定义CircleRectangle类继承自Shape类:

import math

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

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

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

我们可以定义一个函数来计算不同形状的面积:

def calculate_area(shape):
    if isinstance(shape, Shape):
        return shape.area()
    else:
        print("Object is not a valid Shape.")

然后使用CircleRectangle类的实例调用这个函数:

circle = Circle(5)
rectangle = Rectangle(4, 6)

print(calculate_area(circle))
print(calculate_area(rectangle))

输出结果为:

78.53981633974483
24

在这个例子中,Shape抽象基类定义了area方法的规范,CircleRectangle类作为子类实现了这个方法。calculate_area函数接受任何Shape类型(或其子类类型)的对象,并调用其area方法,体现了多态性。同时,通过isinstance检查确保传入的对象确实是Shape类或其子类的实例,这有助于保证程序的正确性。

多态在实际编程中的应用

多态在实际编程中有广泛的应用。例如,在图形绘制库中,可能有不同类型的图形(如圆形、矩形、三角形等),它们都继承自一个基类GraphicObject。这个基类可能定义了一些通用的方法,如draw方法。每个具体的图形类(子类)重写draw方法,以实现自己的绘制逻辑。当需要绘制一组图形时,可以通过一个循环遍历这些图形对象,并调用它们的draw方法,而不需要关心每个对象具体是什么类型的图形,这大大提高了代码的灵活性和可扩展性。

再比如,在游戏开发中,不同类型的角色(如战士、法师、盗贼等)可能继承自一个Character基类。Character基类定义了一些通用的方法,如attackdefend等。每个具体的角色类重写这些方法,以实现各自独特的攻击和防御行为。在游戏逻辑中,当处理角色之间的交互时,可以使用多态来统一处理不同类型角色的行为,使得代码更加简洁和易于维护。

又如,在数据处理和分析中,可能有不同类型的数据来源(如文件、数据库、网络接口等),它们都可以有一个共同的基类DataSourceDataSource基类定义了一些方法,如fetch_data。不同的数据来源类(子类)重写fetch_data方法,以从各自的数据源获取数据。在数据处理流程中,可以通过多态来统一调用不同数据源的fetch_data方法,而无需关心具体的数据来源类型,这使得数据处理代码更加通用和灵活。

综上所述,继承和多态是Python面向对象编程中强大的特性,它们能够提高代码的复用性、可维护性和灵活性,帮助开发者构建更加健壮和高效的程序。在实际编程中,合理运用继承和多态,结合鸭子类型和抽象基类等概念,可以使代码更加符合设计原则,易于理解和扩展。同时,需要注意继承和多态带来的潜在问题,如多重继承可能导致的菱形继承问题,以及过度使用继承可能导致的代码复杂度增加等,通过谨慎的设计和良好的编程习惯来避免这些问题。