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

Python父类方法的重写要点

2024-03-044.0k 阅读

Python 父类方法重写的概念

在 Python 面向对象编程中,继承是一个重要的特性,它允许一个类(子类)从另一个类(父类)获取属性和方法。当子类继承父类时,子类可以使用父类中定义的方法。然而,在某些情况下,子类可能需要对从父类继承来的方法进行修改或定制,以满足自身特定的需求。这种在子类中重新定义父类中已存在方法的行为,就称为方法重写。

例如,假设有一个父类 Animal,其中定义了一个 make_sound 方法用于表示动物发出声音的行为。然后有一个子类 Dog 继承自 Animal,狗发出声音的方式与一般动物有所不同,所以我们需要在 Dog 类中重写 make_sound 方法。

class Animal:
    def make_sound(self):
        print("The animal makes a sound")


class Dog(Animal):
    def make_sound(self):
        print("The dog barks")


# 创建一个 Dog 类的实例
dog = Dog()
dog.make_sound()  

在上述代码中,Dog 类重写了 Animal 类的 make_sound 方法。当我们创建 Dog 类的实例并调用 make_sound 方法时,执行的是 Dog 类中重写后的方法,输出为 "The dog barks"。

重写方法的语法要点

方法签名必须一致

在子类中重写父类方法时,方法的签名(即方法名、参数列表)必须与父类中的方法签名完全一致。这里的参数列表包括参数的个数、参数的顺序以及参数的默认值(如果有的话)。例如:

class Parent:
    def greet(self, name):
        print(f"Hello, {name} from Parent")


class Child(Parent):
    # 正确的重写,方法签名与父类一致
    def greet(self, name):
        print(f"Hello, {name} from Child")


child = Child()
child.greet("Alice")  

如果子类重写方法时改变了参数列表,就不再是重写,而是定义了一个新的方法。例如:

class Parent:
    def greet(self, name):
        print(f"Hello, {name} from Parent")


class Child(Parent):
    # 这不是重写,因为参数列表不同
    def greet(self, name, age):
        print(f"Hello, {name} who is {age} years old from Child")


child = Child()
# 以下调用会报错,因为参数个数不匹配父类方法签名
child.greet("Bob")  

在上述代码中,Child 类中的 greet 方法增加了一个 age 参数,与父类 Parent 中的 greet 方法参数列表不一致,所以它不是对父类 greet 方法的重写。当尝试使用与父类方法签名不一致的参数调用时,就会引发错误。

访问控制的影响

Python 中并没有像其他一些编程语言(如 Java)那样严格的访问修饰符(如 privateprotected)。但通常约定,以单下划线开头的方法(如 _method_name)表示该方法是“受保护的”,虽然在类外部仍然可以访问,但应视为内部使用的方法,子类在重写这些方法时需要谨慎。

以双下划线开头的方法(如 __method_name)会发生名称改写(name mangling),Python 会在方法名前加上类名前缀来避免命名冲突,这种方法在类外部直接访问会比较困难,子类一般也不应该重写这类方法,除非有特殊需求。

例如:

class Base:
    def _protected_method(self):
        print("This is a protected method in Base")

    def __private_method(self):
        print("This is a 'private' method in Base")


class Derived(Base):
    def _protected_method(self):
        print("This is the overridden protected method in Derived")

    # 这里尝试重写“私有”方法,但实际上是定义了一个新的方法
    def __private_method(self):
        print("This is a new method in Derived, not overriding the Base's 'private' method")


derived = Derived()
derived._protected_method()  
# 以下访问会报错,因为“私有”方法发生了名称改写
# derived.__private_method()  

在上述代码中,Derived 类重写了 Base 类的 _protected_method 方法,这种重写是合理的。而对于 __private_method 方法,虽然在 Derived 类中定义了同名方法,但实际上并没有重写 Base 类的 __private_method 方法,因为 __private_methodBase 类中发生了名称改写。

重写方法中的 super() 函数使用

super() 函数在方法重写中起着关键作用,它允许我们在子类的重写方法中调用父类的同名方法。通过 super(),我们可以复用父类方法的部分功能,同时添加子类特有的功能。

  1. 在单继承中的使用:在单继承结构中,super() 函数相对简单直观。例如:
class Shape:
    def __init__(self, name):
        self.name = name

    def describe(self):
        print(f"This is a {self.name}")


class Rectangle(Shape):
    def __init__(self, name, width, height):
        # 使用 super() 调用父类的 __init__ 方法
        super().__init__(name)
        self.width = width
        self.height = height

    def describe(self):
        # 使用 super() 调用父类的 describe 方法
        super().describe()
        print(f"It has width {self.width} and height {self.height}")


rect = Rectangle("Rectangle", 5, 3)
rect.describe()  

在上述代码中,Rectangle 类继承自 Shape 类。在 Rectangle 类的 __init__ 方法中,通过 super().__init__(name) 调用了父类 Shape__init__ 方法来初始化 name 属性。在 describe 方法中,通过 super().describe() 先调用父类的 describe 方法输出基本描述,然后再添加关于矩形宽度和高度的描述。

  1. 在多继承中的使用:在多继承场景下,super() 函数的使用会稍微复杂一些,但它遵循 C3 线性化算法来确定方法解析顺序(MRO)。例如:
class A:
    def method(self):
        print("Method from A")


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


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


class D(B, C):
    def method(self):
        print("Method from D")
        super().method()


d = D()
d.method()  
print(D.mro())  

在上述代码中,D 类继承自 BC,而 BC 又都继承自 A。当调用 d.method() 时,输出顺序为 "Method from D"、"Method from B"、"Method from C"、"Method from A"。super() 函数按照 MRO 顺序依次调用父类的方法。D.mro() 输出的是 [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>],这就是 D 类的方法解析顺序。

重写方法时的注意事项

遵循里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是面向对象编程中的一个重要原则,它指出:所有引用基类(父类)的地方必须能透明地使用其子类的对象。在方法重写时,我们应该确保子类重写后的方法与父类方法具有相同的行为语义,或者至少是兼容的。

例如,假设我们有一个父类 Payment,其中有一个 process_payment 方法用于处理支付。子类 CreditCardPayment 继承自 Payment 并重写 process_payment 方法。如果 CreditCardPaymentprocess_payment 方法处理支付的逻辑与 Paymentprocess_payment 方法预期的行为完全不同,比如父类预期是正常的支付流程,而子类重写后直接拒绝所有支付,这就违反了里氏替换原则。

class Payment:
    def process_payment(self, amount):
        print(f"Processing payment of {amount}")


class CreditCardPayment(Payment):
    # 违反里氏替换原则的重写
    def process_payment(self, amount):
        print("Payment refused")


def make_payment(payment_obj, amount):
    payment_obj.process_payment(amount)


payment = Payment()
credit_card_payment = CreditCardPayment()

make_payment(payment, 100)  
make_payment(credit_card_payment, 100)  

在上述代码中,make_payment 函数期望传入的 payment_obj 无论是父类 Payment 的实例还是子类 CreditCardPayment 的实例,都能按照正常的支付流程处理。但 CreditCardPayment 类重写的 process_payment 方法拒绝支付,这就破坏了里氏替换原则,导致在使用 CreditCardPayment 实例时出现意外行为。

避免重写导致的脆弱性

过度重写或者不合理的重写可能会使代码变得脆弱,难以维护。比如,在父类方法的实现发生变化时,如果子类重写方法没有相应调整,可能会导致功能出错。

例如,假设父类 Vehicle 有一个 move 方法,子类 Car 重写了 move 方法。如果父类 Vehiclemove 方法后来添加了一个新的参数用于表示移动的模式(如 "fast" 或 "slow"),而 Car 类的 move 方法没有相应更新,就可能导致 Car 类的移动行为出现问题。

class Vehicle:
    def move(self):
        print("Vehicle is moving")


class Car(Vehicle):
    def move(self):
        print("Car is moving")


class NewVehicle:
    def move(self, mode="normal"):
        if mode == "fast":
            print("Vehicle is moving fast")
        else:
            print("Vehicle is moving in normal mode")


class NewCar(NewVehicle):
    # 未更新的重写方法,与父类新的 move 方法不兼容
    def move(self):
        print("New Car is moving")


old_car = Car()
old_car.move()  

new_car = NewCar()
# 这里调用 new_car.move() 不会考虑新的 mode 参数,可能导致意外行为
new_car.move()  

在上述代码中,NewCar 类继承自 NewVehicle,但 NewCarmove 方法没有考虑到 NewVehiclemove 方法新增的 mode 参数,这就使得 NewCar 类在使用时可能出现与预期不符的行为,增加了代码维护的难度。

文档化重写的方法

当子类重写父类方法时,应该在子类方法的文档字符串(docstring)中清晰地说明重写的意图、与父类方法的区别以及可能的影响。这样可以帮助其他开发人员理解代码,特别是在团队协作开发的项目中。

例如:

class Animal:
    """动物基类"""
    def make_sound(self):
        """发出动物的一般声音"""
        print("The animal makes a sound")


class Dog(Animal):
    def make_sound(self):
        """重写父类的 make_sound 方法,狗发出叫声
        与父类方法的区别在于:此方法专门用于狗发出叫声,而父类方法是一般动物发出声音的通用描述。
        """
        print("The dog barks")


通过在 Dog 类的 make_sound 方法的文档字符串中清晰地说明重写的情况,其他开发人员在阅读和维护代码时就能更容易理解为什么要重写该方法以及重写后的行为变化。

重写方法与多态

多态的概念与重写的关系

多态是面向对象编程的重要特性之一,它指的是同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在 Python 中,方法重写是实现多态的一种重要方式。

当子类重写父类方法后,在使用父类类型的变量引用子类对象时,调用重写后的方法会表现出不同的行为,从而实现多态。例如:

class Shape:
    def draw(self):
        print("Drawing a shape")


class Circle(Shape):
    def draw(self):
        print("Drawing a circle")


class Square(Shape):
    def draw(self):
        print("Drawing a square")


def draw_shape(shape):
    shape.draw()


circle = Circle()
square = Square()

draw_shape(circle)  
draw_shape(square)  

在上述代码中,draw_shape 函数接受一个 Shape 类型的参数,但实际上可以传入 CircleSquare 类的实例。由于 CircleSquare 类都重写了 Shape 类的 draw 方法,当调用 draw_shape(circle)draw_shape(square) 时,会分别执行 CircleSquare 类中重写后的 draw 方法,从而体现出多态性。

利用多态和重写实现灵活的代码设计

通过合理使用方法重写和多态,可以使代码更加灵活和可扩展。例如,在一个图形绘制的应用程序中,我们可以定义一个 Shape 基类,然后有多个子类如 CircleSquareTriangle 等继承自 Shape 并重写 draw 方法。这样,在绘制图形的逻辑部分,我们可以使用统一的接口(如 draw_shape 函数)来处理不同类型的图形,而不需要为每种图形单独编写绘制逻辑。

class Shape:
    def draw(self):
        pass


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

    def draw(self):
        print(f"Drawing a circle with radius {self.radius}")


class Square(Shape):
    def __init__(self, side_length):
        self.side_length = side_length

    def draw(self):
        print(f"Drawing a square with side length {self.side_length}")


class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def draw(self):
        print(f"Drawing a triangle with base {self.base} and height {self.height}")


def draw_shapes(shapes):
    for shape in shapes:
        shape.draw()


circle = Circle(5)
square = Square(4)
triangle = Triangle(3, 6)

shapes_list = [circle, square, triangle]
draw_shapes(shapes_list)  

在上述代码中,draw_shapes 函数可以处理任何继承自 Shape 类的对象列表,无论将来添加多少种新的图形子类,只要它们重写了 draw 方法,就可以很方便地集成到现有代码中,大大提高了代码的可扩展性和灵活性。

总结 Python 父类方法重写要点

  1. 语法一致性:重写方法的签名必须与父类方法完全一致,包括方法名、参数个数、参数顺序和参数默认值。
  2. 访问控制:对于约定的“受保护”(单下划线开头)方法重写要谨慎,一般不应重写“私有”(双下划线开头)方法。
  3. super() 函数:合理使用 super() 函数来调用父类方法,在单继承和多继承中遵循不同的规则和 MRO 顺序。
  4. 遵循原则:遵循里氏替换原则,确保子类重写方法与父类方法行为语义兼容。
  5. 避免脆弱性:防止因父类方法变化导致子类重写方法出现问题,保持代码的稳定性和可维护性。
  6. 文档化:清晰地文档化重写方法的意图、区别和影响,方便团队协作开发。
  7. 多态应用:利用方法重写实现多态,使代码更加灵活和可扩展。

掌握这些要点,能够帮助开发者在 Python 面向对象编程中更好地利用继承和方法重写特性,编写出高质量、可维护且灵活的代码。