Python 类继承介绍,以及实现 Python 类继承,多重继承,方法重写
前提
阅读本节的前提是对 Python 类以及类的多态性有所掌握,你可以查看Python 类介绍,以及定义和使用 Python 类,编程教程的类的多态性,方法重写,方法重载,方法隐藏介绍来了解相关信息。
继承 Python 类
Python 支持类的继承,你可以定义一个从基类派生的类,其语法的基本形式如下。
class <classname>(<baseclass>):
<block>
- classname 部分
classname
为派生类的名称,他需要符合 Python 的标识符规范,不能使用 Python 关键字或保留关键字。- baseclass 部分
baseclass
是一个返回基类的表达式,其返回的基类应是可访问的,这与使用其他 Python 模块特性(比如,变量,函数)类似。- block 部分
block
为派生类的主体代码,需要使用某种空白字符进行缩进,以表示其归属于派生类。
下面的类GoodMan
继承自类Person
。
# 一个关于人的类
class Person:
title = '该如何称呼哪?'
def __init__(self, name, age):
# 定义类的实例变量 name,age
self.name = name
self.age = age
# 表示好人的类,从 Person 继承
class GoodMan(Person):
pass
判断 Python 对象的类型是否为某个类或其派生类
使用函数isinstance
,你可以判断一个 Python 对象(实例)的类型,是否为某个 Python 类(包括抽象类)或其派生类(子类),或者是否为一组 Python 类中的某个类(包括抽象类)或其派生类(子类),返回True
表示对象的类型是某个类或其派生类,返回False
表示不是。
isinstance(object, classinfo, /)
- object 参数
object
参数为需要判断类型的 Python 对象。- classinfo 参数
classinfo
参数为某个 Python 类,包含 Python 类的元组,或 Python 类的联合类型对象(需要 Python 3.10 或更高版本),联合类型(UnionType
)对象可通过运算符|
组合多个 Python 类获得。
# …
# 创建 GoodMan 的实例
goodman = GoodMan('好心人', 40)
# 判断实例 goodman 的类型是否为 Person 或 int
print(isinstance(goodman, (Person, int)))
# 判断实例 goodman 的类型是否为 int,float,str 中的一个
print(isinstance(goodman, int | float | str))
True
False
判断 Python 类是否为某个类的派生类
使用函数issubclass
,你可以判断一个 Python 类,是否为某个 Python 类(包括抽象类)的派生类(子类),或者是否为一组 Python 类中的某个类(包括抽象类)的派生类(子类),返回True
表示类是派生类,返回False
表示不是。需要指出的是,issubclass
会将一个 Python 类视为其自身的派生类。
issubclass(class, classinfo, /)
- class 参数
class
参数为需要判断是否为派生类的 Python 类。- classinfo 参数
classinfo
参数为某个 Python 类,包含 Python 类的元组,或 Python 类的联合类型对象(需要 Python 3.10 或更高版本),联合类型(UnionType
)对象可通过运算符|
组合多个 Python 类获得。
# …
# 判断类 GoodMan 是否为 object 的子类
print(issubclass(GoodMan, object))
# 判断类 GoodMan 是否为 str 或 Person 的子类
print(issubclass(GoodMan, str | Person))
# 判断类 GoodMan 是否为 GoodMan 的子类
print(issubclass(GoodMan, GoodMan))
True
True
True
搜索 Python 继承链中的特性
如果对象对应的 Python 类中不存在想要访问的特性,可以是指实例特性或类特性,那么 Python 将在继承链的一个或多个基类中搜索,直至此特性被找到,或整个继承链被搜索完毕。对于读取操作(包括调用方法),未搜索到特性会引发异常,对于写入操作,未搜索到特性会导致新的特性被加入 Python 对象。
继承链中 Python 类和其基类之间的搜索顺序,需要通过 Python 的方法解析顺序(Method Resolution Order,MRO)来确定,Python 对象实际对应的类一般处于该顺序的首位。
当然,通过 Python 类访问类特性,与通过 Python 对象访问实例特性或类特性具有类似的搜索机制,只不过其搜索不涵盖 Python 类的实例特性。
无法通过变量类型应用 Python 类的多态性
虽然可以通过:
进一步说明 Python 变量,但你无法真正的为 Python 变量声明一种类型,因此,当通过变量访问 Python 对象的特性时,其查找的起点总是该对象(实例)的实际类型,而不像某些语言一样以变量的类型为起点,这种情况同样适用于 Python 参数,返回值(返回值可以使用->
进行说明)。
下面的类Tree
继承自类Plant
,tree.show()
会调用定义在Tree
类中的show
方法,该方法将访问实例变量name
,虽然Tree
没有直接定义,但name
可以在Plant
中搜索到,这与通过Tree.variety
访问类变量variety
的情况类似。
写入操作tree.age=100
将为tree
添加实例变量age
,因为在此之前age
并不存在。读取操作Tree.name
会导致异常AttributeError
,因为类变量name
不存在。
# 一个关于植物的类
class Plant:
# 类变量 variety
variety = '未知种类'
def __init__(self, name):
# 实例变量 name
self.name = name
# 继承自 Plant 类的类
class Tree(Plant):
# 显示信息的方法
def show(self):
print(self.name)
tree = Tree('大树')
tree.show()
print(f'Tree 是什么种类?{Tree.variety}')
# age 并不存在,写入操作等于为 tree 增加实例变量
tree.age = 100
print(tree.age)
# ERROR 类变量 name 并不存在,读取操作将导致异常
print(Tree.name)
大树
Tree 是什么种类?未知种类
100
…
AttributeError: type object 'Tree' has no attribute 'name'
重写 Python 类的方法
基于上面描述的继承链搜索方式,你可以将 Python 类中定义的所有方法都视为虚方法(Virtual Method),这表示他们可以被重写(Override),如果访问级别允许的话。与模块中定义的 Python 函数一样,Python 类的方法签名默认仅包含名称信息,因此,派生类中定义的方法可以重写基类的同名方法,而不必在意参数的情况。
事实上,重写可针对 Python 类的任意特性,比如变量或属性,而不是仅限于方法。
无法直接对 Python 类的方法进行重载
如上所述,Python 的方法签名仅包含方法名称,这导致你无法直接对 Python 类的方法进行重载,如果希望 Python 类拥有多个名称相同但参数不同的方法,那么需要采用其他的方式,比如,通过functools
模块的singledispatchmethod
类。
下面的Hero
类对Unit
类中的attack
方法进行了两次重写,但只有第二次重写是有效的,第一次重写将被第二次重写覆盖。尝试通过表达式Hero().attack()
调用Unit
类的attack
方法是不可行的,因为只能定位到Hero
中的attack
。
# 一个游戏单位
class Unit:
# 用于发起攻击的方法
def attack(self):
print('Unit 发起攻击')
class Hero(Unit):
# 该方法将被之后定义的 attack 覆盖
def attack(self, times):
while times > 0:
print('Hero 发起攻击')
times -= 1
# 将重写 Unit 的 attack 方法,并覆盖之前定义的 attack
def attack(self, times):
print(f'Hero 将发起 {times} 次攻击')
Unit().attack()
Hero().attack(5)
# ERROR 无法调用 Unit 类定义的 attack 方法
Hero().attack()
Unit 发起攻击
Hero 将发起 5 次攻击
…
TypeError: Hero.attack() missing 1 required positional argument: 'times'
多重继承 Python 类
Python 允许类的多重继承,你可以为一个 Python 类指定多个基类,只需要在定义类的语法中,让baseclass
部分包含使用,
分隔的多个基类即可。
Python 多重继承中的多义性问题
在面向对象编程中,多重继承带来的最大问题就是多义性,Python 的方法解析顺序可有效缓解该问题的发生,因为他尝试将搜索路径线性化,以避免菱形路径(多义性问题)的出现,每一个 Python 类仅被搜索一次,搜索的结果中不会重复包含同一个 Python 类的同一个特性。
至于多个 Python 基类的搜索顺序,由他们在baseclass
部分的书写顺序决定,最先出现的基类优先级最高,在方法解析顺序中的位置靠前,之后出现的基类优先级依次降低,在方法解析顺序中的位置靠后。
在下面的继承关系中,C.name
不会产生多义性问题,虽然B1
,B2
均继承自类A
,但在方法解析顺序中,类A
只会被搜索一次,而不是两次,因此类变量name
不是模棱两可的。
语句c.show()
将调用B2
的show
方法,虽然B1
书写在B2
之前,B1
拥有更高的优先级,但类A
的优先级并不在B1
和B2
之间,而是排在B2
之后。
class A:
name = 'A'
def show(self):
print('调用 A 的 show 方法')
class B1(A):
pass
class B2(A):
def show(self):
print('调用 B2 的 show 方法')
class C(B1, B2):
pass
# 不会产生多义性问题,因为类 A 仅被搜索一次
print(C.name)
c = C()
# 调用 B2 的 show 方法,而不是 A 的 show 方法
c.show()
A
调用 B2 的 show 方法
调用 Python 基类中的方法
如果某个 Python 类重写了基类中的方法,并且希望继续使用基类所实现的功能,那么可以在重写的方法中,使用super
类来调用基类的方法,该类实现了一种访问的代理功能,你可以像通过 Python 实例或类访问特性一样,通过super
类的实例(对象)来访问特性,只不过super
类需要指定特性的搜索起点,以实现对特定基类的访问。
super()
super(type, object_or_type=None, /)
- type 参数
type
参数是一个 Python 类,在方法解析顺序中,位于该 Python 类之后的第一个类将成为特性的搜索起点。- object_or_type 参数
object_or_type
参数为需要搜索特性的 Python 实例或类,他们应该是type
参数所表示的 Python 类或其派生类,或者是type
参数所表示的 Python 类或其派生类的实例。如果忽略该参数,那么得到的super
对象将处于未绑定状态,他可能无法完成你的预期目标。
对于不带有参数的super
类的构造器,仅能在 Python 类的实例方法或类方法中使用,他的效果等同于书写super(type,self)
,super(type,cls)
,其中type
为当前类,self
为实例方法的self
参数,cls
为类方法的cls
参数。
对于带有参数的super
类的构造器,可以在 Python 类的方法或其他位置使用,如果是类的静态方法,那么需要采用形式为super(type,class)
的表达式,其中type
为用于指示搜索起点的 Python 类(方法解析顺序中,排在type
对应的类之后的第一个类将作为搜索起点),class
为当前类。由于指定了特性的搜索起点,因此,该构造器可实现更为细致的基类访问。
当然,不仅是基类中的方法,通过super
类访问其他特性也是可行的,比如变量,属性。
下面的代码,演示了如何通过super
调用基类的实例方法,类方法和静态方法。其中,语句super(Dog,self).run()
使BigDog
绕过了基类Dog
中的run
方法,语句super(BigDog,dog).run()
则调用了定义在Dog
中的run
方法。
class Animal:
@staticmethod
def show():
print('这里是 Animal!')
@classmethod
def eat(cls, something):
print(f'Animal 吃点 {something}')
def run(self):
print('Animal 奔跑起来了!')
class Cat(Animal):
@staticmethod
def show():
print('Cat 调用 Animal 的静态方法 show')
super(Cat, Cat).show()
@classmethod
def eat(cls, something):
print('Cat 调用 Animal 的类方法 eat')
super().eat(something)
def run(self):
print('Cat 调用 Animal 的实例方法 run')
# 相当于 super(Cat, self).run()
super().run()
class Dog(Animal):
def run(self):
print('Dog 奔跑起来了!')
class BigDog(Dog):
def run(self):
print('BigDog 调用 Animal 的 run 方法')
super(Dog, self).run()
Cat.show()
Cat.eat('小鱼干')
Cat().run()
dog = BigDog()
dog.run()
# 调用 Dog 中的 run 方法
super(BigDog, dog).run()
Cat 调用 Animal 的静态方法 show
这里是 Animal!
Cat 调用 Animal 的类方法 eat
Animal 吃点 小鱼干
Cat 调用 Animal 的实例方法 run
Animal 奔跑起来了!
BigDog 调用 Animal 的 run 方法
Animal 奔跑起来了!
Dog 奔跑起来了!
Python 的 super 对象可能不支持 [] 运算符
由于super
类仅是一种访问代理,因此他可能并不支持你所期望的特性访问方式。比如,在 Python 类中定义方法__getitem__
之后,可以对该类的实例使用[]
运算符,但对于super
来说,仅可通过.
运算符调用__getitem__
,使用[]
运算符将导致异常TypeError
。
下面的类Store
虽然定义了方法__getitem__
,但只能通过super
和.
运算符来调用他。
# 一个表示商店的类
class Store:
def __getitem__(self, key):
print(f'想获得物品?{key}')
return key
# 一个表示应用商店的类
class AppStore(Store):
pass
store = AppStore()
# 可以对实例使用 [] 运算符
x = store['x']
# 对 super 需要使用 . 运算符
y = super(AppStore, store).__getitem__('y')
# ERROR 对 super 使用 [] 运算符,将导致异常
z = super(AppStore, store)['z']
想获得物品?x
想获得物品?y
…
TypeError: 'super' object is not subscriptable