在另一个类的基础上构建类
继承让你把一个新类定义为已有类的扩展。已有的类是父类(也叫基类或超类),新类是子类。子类无需重复编写,就能获得父类的所有方法和类属性,还可以添加自己的方法,或者替换那些需要有不同行为的方法。
在类名后面的括号里声明父类:
输出:
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)。拿不准时,组合以后更容易修改。