前情提要

面向对象编程的三大特性分别是封装、继承、多态,其核心思想在任何语言中都是相通的。如果对继承和多态的基本概念还不太清楚,可以阅读 Java 篇笔记继承多态。了解它们“是什么”和“为什么”,我相信读完这两篇笔记会对这两个概念有更好的原理和内存上的认识。本文聚焦 Python 中的具体语法、特性以及和 Java、C++ 的细微差异,避免混淆。


1. 为什么要用继承?

在定义类时,你一定会发现:很多类之间存在明显的“共性”。比如 Dog(狗)和 Cat(猫)都属于动物,它们都有 nameage 属性,也都有 eat()sleep() 这样的行为。如果不用继承,每个类都要把这些公共的属性和方法完整地再写一遍,代码冗余且难维护。

继承的核心价值:

  • 提高代码复用性:将多个子类的重复代码抽取到父类中,子类无需再写一遍,直接继承即可使用。
  • 便于功能扩展:子类在拥有父类全部功能的基础上,还能增加自己特有的属性和方法。
  • 形成清晰的层次体系:符合现实世界“is-a”的关系,让代码结构更贴合业务逻辑。

举个反例体会一下:

# 没有继承,Dog 和 Cat 要各自写一遍 name, age 和 eat()
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def eat(self):
        print(f"{self.name} 正在吃骨头")

class Cat:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def eat(self):
        print(f"{self.name} 正在吃鱼")

你会发现 __init__ 里的两行代码重复了。这就是我们要解决的。


2. 继承的概念与语法

2.1 什么是继承

继承是面向对象三大特性之一。它允许我们基于一个已有的类(父类)来创建一个新的类(子类)。
子类会自动拥有父类中可继承的属性和方法,并可以在此基础上添加新的成员,或者说可以把多个子类中重复的代码抽取到父类中。

2.2 Python 继承语法

Python 的继承语法非常简洁,在类定义时把父类名写在括号里即可。

单继承:

# 父类(动物)
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def eat(self):
        print(f"{self.name} 正在吃东西")

# 子类(狗)继承 Animal
class Dog(Animal):
    def bark(self):
        print(f"{self.name} 汪汪叫")

使用:

dog = Dog("旺财", 3)
dog.eat()   # 继承自父类的方法
dog.bark()  # 子类自己的方法

多继承:
Python 支持多继承,一个子类可以同时继承多个父类。

class Flyable:
    def fly(self):
        print("飞起来了")

class Swimmable:
    def swim(self):
        print("正在游泳")

# 鸭子既会飞又会游,可以同时继承两个类
class Duck(Animal, Flyable, Swimmable):
    pass

duck = Duck("唐老鸭", 2)
duck.eat()   # 来自 Animal
duck.fly()   # 来自 Flyable
duck.swim()  # 来自 Swimmable

⚠️ 多继承虽然灵活,但要注意“菱形继承”问题(多个父类有同名方法时调用顺序不确定)。Python 通过 MRO(方法解析顺序)解决,后面会详细说明。

2.3 与 Java / C++ 语法对比

特性 Python Java C++
单继承 class Child(Parent): class Child extends Parent class Child : public Parent
多继承 ✅ 支持,多个父类写在括号里 ❌ 不支持(可通过接口多实现) ✅ 支持,用逗号分隔
访问父类方法 super().方法名() super.方法名() 父类名::方法名()this->父类::方法()
构造方法继承 __init__ 可被继承(特殊规则见下文) 构造函数不被继承,必须显式定义 构造函数不被继承,但默认调用父类无参构造

Java 和 C++ 的构造函数名与类名相同,因此如果子类直接“继承”了父类的构造函数,在语法上就会出现类名不匹配的矛盾,所以不支持继承。Python 的初始化方法固定为 __init__,不存在命名冲突,因此有了不同的表现,详情见下文。


3. Python 继承的特点

3.1 多继承与多层继承

  • 多继承:一个子类有多个父类。
  • 多层继承:A 派生出 B,B 派生出 C,C 既拥有 A 的方法也拥有 B 的方法。
class A:
    def method_a(self):
        print("A")

class B(A):
    def method_b(self):
        print("B")

class C(B):   # C 间接继承 A
    pass

c = C()
c.method_a()  # 正常调用
c.method_b()

3.2 子类到底继承了父类的哪些东西?

我们需要区分 “继承” 和 “访问权限” 两个概念:只有先继承了,才能讨论是否有权访问。

在 Java 中,方法的继承依赖虚方法表(vtable):子类会把父类中可继承的方法条目拷贝到自己的虚方法表里,而 private 方法根本不进入虚方法表,因此我们说 Java 私有方法不可继承。属性方面,子类对象的内存中会包含父类属性的副本,这一点与 Python 是相通的。
但 Python 的方法调用完全不走虚方法表,而是通过 MRO(方法解析顺序) 动态查找类字典。两种语言的底层机制不同,导致继承的表现也有差异,我们来详细拆解。

3.2.1 方法是如何“继承”的?

Python 的方法查找机制

  • 每个类都有一个 __dict__,里面存着自己定义的方法。
  • 当通过实例 obj.method() 调用方法时,Python 会:
    1. type(obj).__dict__(即子类的字典)里找该方法;
    2. 如果找不到,沿着 MRO 链路依次去父类、祖父类的 __dict__ 中找;
    3. 找到后,将函数包装成绑定方法并调用。
  • 因此,子类不需要在自己的字典里“拷贝”父类的方法,它只需要一条能通往父类字典的查找路径。 只要父类字典里存在该方法,子类对象就能调用。

3.2.2 属性是如何“继承”的?

属性的继承主要体现在初始化过程实例的 __dict__ 中。

  • 非私有属性:父类的 __init__ 中通过 self.name = value 设置的属性,最终会被添加到当前实例的 __dict__ 中。
    如果子类没有自定义 __init__,创建子类对象时会自动调用父类的 __init__,于是这些属性就直接写入了子类实例的字典里(每个子类对象都拥有一份自己独立的属性数据)
    这与 Java 类似:子类对象内存中包含父类成员变量的副本。
  • 私有属性(self.__data:同样会被存入实例 __dict__。子类实例里确实有这个属性,但因为是私有属性无法通过 self.__data 访问,必须通过父类提供的 get/set 方法或改写后的名字。
    因此,私有属性也被继承了(存在于实例中),只是访问权限受限。

注意:属性数据存储在每个实例自己的 dict 里,而不是通过 MRO 去父类字典查找(父类字典里存放的是方法和类属性)。所以属性的“继承”实际上是指“子类对象在初始化时,会根据父类的逻辑创建这些属性”。这与方法的查找机制有本质区别。

3.2.3 子类继承与访问情况一览表

父类成员类型 是否被子类继承? 子类能否直接访问? 说明
构造方法 __init__ ✅ 可以继承 ✅ 可以直接使用(当子类未定义自己的 __init__ 时) 若子类自定义了 __init__,父类 __init__ 不会自动调用,需要显式 super().__init__()
公有属性 (self.name) ✅ 继承(实例字典中存在) ✅ 直接访问 通过父类 __init__ 在子类实例上创建
私有属性 (self.__data) ✅ 继承(实例字典中存在,但被改名) ❌ 不能直接通过原名访问 需通过父类预留的公有 get/set 方法间接访问
公有方法 (def method(self)) ✅ 继承(通过 MRO 可查找) ✅ 直接调用/可重写 子类可通过 super().method() 调用父类版本
私有方法 (def __method(self)) ✅ 继承(存在于父类字典,MRO 可达) ❌ 不能直接通过原名调用 被名称改写为 _父类名__method,可通过公有的父类方法间接调用或使用改写后的名字

理解了 Python 在属性与方法继承上的机制,接下来我们就可以探讨构造方法 __init__ 的具体继承规则了。

3.2.4 构造方法 init 的继承问题

Java / C++ 的规则回顾:

  • 构造函数名与类名相同,子类不会继承父类的构造函数。
  • Java 中如果子类构造函数没有显式调用 super(…),编译器会自动插入父类的无参构造调用;若父类没有无参构造,则必须手动调用有参版本。
  • C++ 中,子类构造函数会默认调用父类的无参构造函数,如果需要传参,必须写在初始化列表中。

Python 为什么“可以继承” __init__
因为 Python 中,__init__ 是一个初始化方法,在对象创建后自动被调用。它的名字是固定的 __init__,所以它本质上就是一个魔法方法。既然它是方法,子类自然可以把它当作普通方法继承下来。

具体规则:

  1. 如果子类没有定义 __init__,那么创建子类对象时,会自动调用父类的 __init__,完成父类属性的初始化。
  2. 如果子类定义了自己的 __init__,则父类的 __init__ 不会被自动调用。这时如果还需要父类的初始化逻辑,必须显式调用 super().__init__(...)父类名.__init__(self, ...)

示例 1:子类无新增属性,完全不用写 __init__

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

class Dog(Animal):
    # 没有 __init__,自动继承父类的
    def bark(self):
        print(f"{self.name} 汪汪叫")

dog = Dog("大黄")   # 直接传参,因为 Dog 继承了 Animal 的 __init__
print(dog.name)     # 大黄
dog.bark()

示例 2:子类有新增属性,需要自己写 __init__ 并调用 super()

class Dog(Animal):
    def __init__(self, name, breed):   # 新增 breed 参数
        super().__init__(name)         # 先调用父类 __init__ 初始化 name
        self.breed = breed             # 初始化自己的新属性

    def show_info(self):
        print(f"{self.name}{self.breed}")

dog = Dog("小白", "金毛")
dog.show_info()

如果不写 super().__init__(name)self.name 就没有被赋值,后续使用会出错。


4. 方法重写

4.1 什么是重写

子类从父类继承了一个方法,但觉得父类的实现不满足自己的需求,就可以在子类中重新定义一个方法名和参数列表完全相同的方法,这就是重写

重写后,通过子类对象调用该方法时,执行的是子类的版本,父类的版本被覆盖了。

class Animal:
    def make_sound(self):
        print("动物发出声音")

class Cat(Animal):
    def make_sound(self):          # 重写父类方法
        print("喵喵喵")

cat = Cat()
cat.make_sound()   # 喵喵喵

重写时,方法签名(名称和参数)必须一致。

4.2 super() 的使用与 MRO

即使子类重写了父类方法,很多时候我们还是需要在子类的方法中调用父类的原始实现,然后再添加自己的逻辑。这时就用 super()

super() 的本质是返回一个代理对象,它会按照 MRO(Method Resolution Order,方法解析顺序) 去查找下一个类。

经典场景:__init__ 中的 super()
前面已经见过,这是最常见的形式。

其他方法中调用父类版本:

class Parent:
    def greet(self):
        print("Hello from Parent")

class Child(Parent):
    def greet(self):
        super().greet()     # 调用父类 greet
        print("Hello from Child")

c = Child()
c.greet()
# 输出:
# Hello from Parent
# Hello from Child

MRO 是什么?
在多继承的情况下,多个父类可能存在同名方法。Python 使用 C3 线性化算法 计算 MRO,确定属性或方法的查找顺序。可以通过 类名.__mro__ 查看。

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__)
# (D, B, C, A, object)

就近原则:当子类和父类有同名属性或方法时,按照 MRO 顺序,先找到的就使用。super() 也是沿着 MRO 链条寻找下一个类。在单继承中它代表父类,在多继承中则代表 MRO 链中的下一个类,默认是继承的第一个父类。


5. 多态:同一个接口,不同的行为

5.1 为什么需要多态

假设你在开发一个教务系统,已经定义了 TeacherStudentAdmin 三个类,现在需要实现一个“通用注册”功能:用户无论是什么身份,都能调用同一个注册入口,但实际行为可能略有不同(比如老师需要输入工号,学生需要输入学号)。

不用多态的笨办法:为每种类型写一个注册函数,调用时根据类型判断,代码臃肿且不易扩展。
用多态:定义一个 Person 父类,让 TeacherStudentAdmin 都继承它。注册方法接受一个 Person 类型的参数,具体传进来的是老师还是学生,由对象自己决定行为。

5.2 传统多态的三个前提

在 Java、C++ 等静态类型语言中,多态的实现必须满足三个条件:

  1. 有继承/实现关系:存在父类和子类,或接口与实现类。
  2. 有方法重写:子类必须重写父类的方法。
  3. 有父类引用指向子类对象:即用父类类型的变量接收子类实例,如 Person p = new Student();
// Java 多态示例
class Person {
    void register() { System.out.println("人员注册"); }
}
class Student extends Person {
    @Override
    void register() { System.out.println("学生注册,需填写学号"); }
}
class Teacher extends Person {
    @Override
    void register() { System.out.println("教师注册,需填写工号"); }
}

public class Main {
    public static void doRegister(Person p) {  // 父类引用
        p.register();                           // 实际调用哪个方法由对象决定
    }
    public static void main(String[] args) {
        doRegister(new Student());   // 学生注册,需填写学号
        doRegister(new Teacher());   // 教师注册,需填写工号
    }
}

5.3 Python 的“鸭子类型”多态

Python 是一门动态类型语言,它实现多态的方式更加灵活,甚至不需要严格的继承关系。Python 奉行“鸭子类型”(Duck Typing):

当看到一只鸟走起来像鸭子、游泳像鸭子、叫起来像鸭子,那么这只鸟就可以被称为鸭子。

换句话说,一个对象的可用性不取决于它的类型,而是取决于它是否具有所需的方法和属性。只要一个对象实现了某个方法,就可以把它当作“具备该行为的对象”来使用,函数传参时不需要声明类型。

示例:完全不用继承的多态

class Dog:
    def speak(self):
        print("汪汪")

class Cat:
    def speak(self):
        print("喵喵")

class Duck:
    def speak(self):
        print("嘎嘎")

def animal_sound(animal):      # 没有类型限制
    animal.speak()             # 只要 animal 有 speak() 方法就行

animal_sound(Dog())   # 汪汪
animal_sound(Cat())   # 喵喵
animal_sound(Duck())  # 嘎嘎

即使 DogCatDuck 之间没有任何继承关系,只要它们都实现了 speak()animal_sound 就能正常工作。这种动态多态极大地增加了代码的灵活性。

当然,结合继承和重写的传统多态在 Python 中同样适用,而且能让代码结构更清晰。例如前面的 Person 例子改写为 Python:

class Person:
    def register(self):
        print("人员注册")

class Student(Person):
    def register(self):
        print("学生注册,需填写学号")

class Teacher(Person):
    def register(self):
        print("教师注册,需填写工号")

def do_register(person: Person):
    person.register()

do_register(Student())   # 学生注册,需填写学号
do_register(Teacher())   # 教师注册,需填写工号

这里的类型提示 person: Person 只起注解作用,并不强制类型,依然遵循鸭子类型。

总结

  • 静态语言的多态靠“继承 + 重写 + 父类引用”
  • Python 的多态靠“对象是否具备所需方法”,本质是鸭子类型,但通常我们仍然会使用继承来复用代码和表达逻辑关系。

以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。希望这篇文章能帮你把 Python 中的继承和多态彻底理清,不再和 Java/C++ 混淆。学习面向对象,当你掌握了“为什么”,再去记“怎么写”就会轻松许多。如果文章对你有帮助,别忘了点个赞、留个言,让更多的小伙伴看到~ 我们下篇再见!

更多推荐