Menu

Python 继承:子类、super()、方法重写与 MRO

Python 中类的继承是怎么工作的:创建子类、调用 super().__init__、重写和扩展方法、isinstance 和 issubclass、多重继承与 MRO,以及什么时候组合是更好的选择。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

在另一个类的基础上构建类

继承让你把一个新类定义为已有类的扩展。已有的类是父类(也叫基类或超类),新类是子类。子类无需重复编写,就能获得父类的所有方法和类属性,还可以添加自己的方法,或者替换那些需要有不同行为的方法。

在类名后面的括号里声明父类:

输出:

Rex is an animal
Woof

Dog 自己既没有 __init__,也没有 describe。当你调用 Dog("Rex") 时,Python 先在 Dog 上找 __init__,没找到,于是使用 Animal 上的那个。rex.describe() 也是同样的查找过程。而对于 rex.speak(),Python 先在 Dog 上找到了 speak,就停在那里。

这个查找顺序就是全部的机制:Python 先搜索对象自己的类,然后是父类,再然后是父类的父类,使用第一个找到的结果。如果你刚接触类,可以先看类的页面,那里介绍了 __init__、self 和方法。

重写方法

子类中与父类同名的方法,会在子类的实例上取代父类的方法。这叫做重写(override)。每个子类都可以用自己的方式响应同一个调用:

输出:

Rectangle with area 12
Square with area 25
Shape with area 0

summary 只在 Shape 上定义了一次,但它调用了 self.area()。由于 self 是实际的对象,Rectangle 会运行 Rectangle.area,而 Square 会在它的父类 Rectangle 上找到 area。父类的代码调用了子类的方法,却根本不知道子类的存在。正是这一点让继承非常适合共享通用流程,同时让每个子类填充各自的细节。

用 super() 调用父类

当子类定义了自己的 __init__ 时,父类的 __init__ 就不会再自动运行。如果子类仍然需要父类的初始化,就用 super().__init__() 调用它:

输出:

Ada 1050.0

super() 返回一个对象,它从当前类的父类开始查找方法。因此 super().__init__(owner, balance) 会在同一个对象上运行 Account.__init__,之后 self.owner 和 self.balance 就存在了。

漏掉这一行,对象就只构建了一半:

输出:

0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'

你也可以写成 Account.__init__(self, owner),旧代码里经常这样写。更推荐用 super(),因为它不用重复父类的名字,而且能和多重继承正确配合,下文会讲到。

扩展方法而不是替换它

super() 可以用在任何方法中,不只是 __init__。在重写的方法里调用父类的版本,可以在它的基础上添加行为,而不必重写一遍:

输出:

started
[12:00] started
INFO [12:00] started

每一层只添加一部分,其余的交给上一层处理。如果以后父类的 format 改了,两个子类都会自动跟着变。

isinstance 和 issubclass

子类的实例同时也是它上面每个类的实例。isinstance(obj, cls) 检查这种关系,issubclass(child, parent) 则检查两个类之间的关系:

优先使用 isinstance,而不是直接比较 type(obj)。像 type(obj) is Animal 这样的检查会拒绝所有子类,一旦有人添加了 Dog,它就会出问题。

你写的每个类都继承自 object,即使你没有明确写出来。class Animal: 和 class Animal(object): 是一样的。__str__、__repr__ 和 __eq__ 等默认方法就来自这里,这也是为什么重写 __str__ 会改变 print() 显示的内容:

输出:

(2, 3)
True

要求子类实现某个方法

有时父类不应该被直接使用,只作为模板。abc 模块可以把这样的类标记为抽象类。所有用 @abstractmethod 装饰的方法都必须被重写,这个类才能被实例化:

输出的前三行是导出结果:

exporting 2 rows
1,2
3,4

最后一行是一个 TypeError,它会指出 Exporter 和缺失的 export 方法(具体措辞因 Python 版本而异)。错误在创建对象时就会暴露出来,而不是等到最终调用 export 时才出现。

多重继承与 MRO

一个类可以列出多个父类,它会同时继承所有父类:

输出:

paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']

两个父类都定义了 move,Duck 得到的是 Swimmer 的那个。Python 通过方法解析顺序(MRO)来决定:这是一个存放在 Duck.__mro__ 中的类列表,每次属性查找都会从前往后遍历它。它从类本身开始,保持父类按你列出的顺序排列,让每个类都排在它自己的父类之前,最后以 object 结尾。把顺序换成 class Duck(Flyer, Swimmer),move 就会返回 "flies"。

super() 也遵循同一个列表。在 Swimmer 的某个方法中,super() 的意思是“在对象所属类的 MRO 中排在 Swimmer 后面的下一个类”,对于 Duck 来说就是 Flyer,而不是 object。正因如此,相互协作的类可以各自调用 super(),让链上的每个类恰好运行一次,即使在两个父类拥有同一个祖父类的菱形继承中也一样。

在实践中,多重继承主要用于 mixin:一种只添加一项能力的小类,比如添加 to_json 方法的 JsonMixin,再和一个主要的父类组合使用。带有多个完整父类的深层继承体系难以理解,也很少真正需要。

继承还是组合?

继承表示“是一个”的关系。SavingsAccount 是一个 Account:任何能处理 Account 的代码,都应该无需修改就能处理它。如果这句话听起来不对,你需要的可能是组合,它表示“有一个”的关系:一个对象把另一个对象存为属性,并调用它。

输出:

Roadster: 300 hp engine running

class Car(Engine) 也能运行,但它等于宣称汽车是一种发动机,让 Car 拥有发动机的所有方法,而且不换一个类就没法换装另一种发动机。用组合的话,各个部件保持独立,换发动机只需要传入另一个对象。

几个说明继承用错了地方的信号:

  • 子类重写了父类的大部分方法,或者禁用了其中一些。
  • 你继承只是为了复用一两个辅助方法。
  • 继承层级超过两三层,你得翻好几个文件才能看懂一个方法在做什么。

常见错误

  • 忘记调用 super().__init__()。 子类的 __init__ 取代了父类的,所以父类的属性从未被设置,之后就会遇到 AttributeError。
  • 在 super() 调用中传入 self。 要写 super().__init__(name),而不是 super().__init__(self, name)。super() 已经把方法绑定到当前对象上了。
  • 重写时签名不匹配。 如果父类的 speak(self) 被公共代码调用,子类的 speak(self, volume) 多了一个必填参数,就会让那段代码出错。重写的方法要保持兼容,或者给新参数设置默认值。
  • 检查 type(obj) == SomeClass。 它会忽略子类。请用 isinstance。
  • 用继承在不相关的类之间共享代码。 应该把共享的代码移到一个函数或辅助对象中。

常见问题

Python 中怎么继承一个类?

在新类名后面的括号里写上父类:class Dog(Animal):。Dog 会获得 Animal 的所有方法和类属性,还可以添加新的方法或重写已有的方法。

super().__init__() 有什么作用?

它调用父类的 __init__,让父类设置好自己的属性。当子类定义了自己的 __init__ 时,Python 不会自动调用父类的,所以你要用父类需要的参数调用 super().__init__(...),然后再设置子类额外的属性。

Python 中 isinstance 和 type 有什么区别?

对于 Animal 的实例以及 Animal 任何子类的实例,isinstance(obj, Animal) 都是 True。而只有对象直接由 Animal 本身创建时,type(obj) is Animal 才是 True。优先使用 isinstance,因为别人添加子类后它仍然能正常工作。

Python 支持多重继承吗?

支持:class C(A, B): 同时继承两个类。当两个父类定义了同名方法时,Python 按照方法解析顺序(MRO)来选择,你可以用 C.__mro__ 查看。它把 C 排在最前面,然后从左到右列出父类,object 排在最后。

什么时候应该用组合而不是继承?

继承用于“是一个”的关系,即子类可以用在任何需要父类的地方(SavingsAccount 是一个 Account)。组合是把一个对象存为属性,用于“有一个”的关系(Car 有一个 Engine)。拿不准时,组合以后更容易修改。

Coddy 编程语言插图

和 Coddy 一起学编程

开始