一、继承与多态
在面向对象(OOP)程序设计中,当我们定义一个类的时候,可以从某个现有的类继承,新的类称为子类(Subclass),而被继承的类称为基类、父类或超类(Base class、Super class)。类中继承就是子类获得父类的一些方法和属性(类属性、实例属性、类方法、实例方法、静态方法),这里使用一些也就是说有些是子类继承不到的,比如私有属性。
简单来说继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。另外在 Python 2.4 开始,Python 引入了 object 这个最上层的根类,但需要显式指定,也被称之为经典类;而在 Python 3 中所有类默认都继承 object 根类,无需显式指定,也被称之为新式类。
1 2 3 4 5 6 7 |
# Python 2中写法; class A(object): pass # Python 3中写法; class A: pass |
比如,编写一个 Base 类,定义一个 init 方法:
1 2 3 |
class Base(): def init(self): print('Base class') |
当我们需要编写 Sub1 和 Sub2 类时,就可以直接从 Base 类继承:
1 2 3 4 5 |
class Sub1(Base): pass class Sub2(Base): pass |
对于 Sub1 和 Sub2 来说,Base 就是它的父类;对于 Base 来说,Sub1 和 Sub2 就是它的子类。
继承有什么好处?最大的好处是子类获得了父类的一些属性和方法。由于 Base 实现有 init 方法,因此,Sub1 和 Sub2 作为它的子类,什么事也没干,就自动拥有了 init:
1 2 3 |
>>> A = Sub1() >>> A.init() parent class |
当然,子类也可以新增自己特有的方法,也可以把父类不适合的方法覆盖重写。
那么什么样的属性是子类无法继承的呢?就是以__
开头的私有变量是子类无法继承的,如下:
1 2 3 4 5 6 7 8 |
class Base(): def __init__(self): self.x = 'Base' self.__y = 'Base' class Sub(Base): def print_y(self): print(self.__y) |
结果如下:
1 2 3 4 5 6 7 8 |
>>> B = Sub() >>> B.x 'Base' >>> B.print_y() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in print_y AttributeError: 'Sub' object has no attribute '_Sub__y' |
我们知道私有变量内部是通过改变变量名来实现的,从报错结果来看就是因为变量名问题,当然通过特殊方法也是可以继承到的,但是通常不这么干。
下面再来尝试一个带参数的类:
1 2 3 4 5 6 |
class Base(): def __init__(self, x): self.x = x class Sub(Base): pass |
实例化子类:
1 2 3 4 |
>>> A = Sub() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __init__() missing 1 required positional argument: 'x' |
报错了,告诉我们需要传参数,也就说明,当我们初始化子类时会先初始化父类。当父类定义了带参数的初始化方法时,子类要显式的定义初始化方法,并且在初始化方法里初始化父类,如下:
1 2 3 |
class Sub(Base): def __init__(self): super(Sub, self).__init__(1) |
注意在 Python 3 里语法有所改变:你可以用super().__init__()替换super(Sub, self).__init__(),更简洁,更 Nice。
使用 super 用来返回 super 对象(super 是 Python 3 内置用在类继承中的方法),可以使用 super 对象调用父类的方法及属性。再次实例化子类,如下:
1 2 3 |
>>> A = Sub() >>> A.x 1 |
通过使用 super,就可以正常实例化子类了;当然在实例化子类时,你也可以直接给子类传参,如下:
1 2 3 |
class Sub(Base): def __init__(self, x): super().__init__(x) |
1 2 3 |
>>> A = Sub(1) >>> A.x 1 |
但是通常不这么干,为了代码易读易用。
另外,super 还有一个作用就是当父类和子类有相同的方法名称时,其实父类不会生效,此时可以通过 super 来初始化父类。测试代码如下:
1 2 3 4 5 6 7 |
class Base(): def __init__(self): self.x = 2 class Sub(Base): def __init__(self): self.y = self.x * 2 |
然后,我们在实例化子类时就会报找不到 self.x 属性,如下:
1 2 3 4 5 |
>>> A = Sub() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in __init__ AttributeError: 'Sub' object has no attribute 'x' |
然后我们使用 super 在子类中初始化一下父类就可以正常识别父类属性了。
1 2 3 4 |
class Sub(Base): def __init__(self): super(Sub, self).__init__() self.y = self.x * 2 |
但是一般也不要这么写,在子类如果不使用跟父类相同名称的方法名就没这个问题。关于 super 更多细节可以看相关文档。
继承的第二个好处需要我们对代码做一点改进;通过上面的例子,我们看到了,既然是继承,我们得到了想要的结果。但是当子类和父类都存在相同的方法或属性时,我们说,子类的方法或属性会覆盖了父类的方法或属性,在代码运行的时候,总是会调用子类的方法或属性。这样,我们就获得了继承的另一个好处:多态。
多态顾名思义是指“多种形态”,具体是指父类定义的方法可以调用子类实现的方法。不同的子类有不同的实现,从而给父类的方法带来了多样的不同行为。换种方式描述,主程序只调用接口或缺省类,而使用者提供实现类,两者结合起来,完成业务功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Base: def init(self): self.example() def example(self): print('Base class') class Sub(Base): def example(self): print('Sub class') --------------------- >>> a = Base() >>> a.init() Base class >>> b = Sub() >>> b.init() Sub class |
父类 Base 定义的 init 方法调用了子类实现的 example 方法,子类的方法可以对父类定义的方法进行覆盖,父类的 example 方法被隐藏起来了。
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个 class 的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和 Python 自带的数据类型,比如 str、list、dict 没什么两样:
1 2 3 |
A = Base() # A是Base类型; B = Sub() # B是Sub类型; C = "test" # C是str类型; |
判断一个变量是否是某个类型可以用 isinstance() 判断,如下:
1 2 3 4 5 6 |
>>> isinstance(A, Base) True >>> isinstance(B, Sub) True >>> isinstance(C, str) True |
看上去 A、B、C 对应 Base、Sub、str 三种类型都没有问题,接下来测试一下 B 是否是 Base 类型:
1 2 |
>>> isinstance(B, Base) True |
也为真,这说明不仅 B 是 Sub 类型,同时也是 Base 类型。
不过仔细想想,这是有道理的,因为 Sub 是从 Base 继承下来的,当我们创建了一个 Sub 的实例 B 时,我们认为 B 的数据类型是 Sub 没错,但 B 同时也是 Base 也没错,Sub 本来就是 Base 的一种!
所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行:
1 2 |
>>> isinstance(A, Sub) False |
Sub 可以看成 Base,但 Base 不可以看成 Sub。
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个 Base 类型的变量:
1 2 |
def run(Base): return Base.init() |
当我们传入 Base 的实例时,run() 就打印出:
1 2 |
>>> run(Base()) Base class |
当我们传入 Sub1 的实例时,run() 就打印出:
1 2 |
>>> run(Sub()) Sub1 class |
看上去没啥意思,但是仔细想想,现在,如果我们不管定义多少个从 Base 派生的子类,不必对 run() 做任何修改。实际上,任何依赖 Base 作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处就是,当我们需要传入 Sub、Sub1、Sub2……时,我们只需要接收 Base 类型就可以了,因为 Sub、Sub1、Sub2…… 都是 Base 类型,然后,按照Base类型进行操作即可。由于 Base 类型有 init 方法,因此,传入的任意类型,只要是Base类或者子类,就会自动调用实际类型的 init 方法,这就是多态的意思。
对于一个变量,我们只需要知道它是 Base 类型,无需确切地知道它的子类型,就可以放心地调用 init 方法,而具体调用的方法或属性是作用在 Base、Sub 还是 Sub1 对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种 Base 的子类时,只要确保方法或属性编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增 Base 子类;
对修改封闭:不需要修改依赖 Base 类型的 run() 等函数。
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类 object,这些继承关系看上去就像一颗倒着的树。
二、多重继承
Python 是支持类中多重继承的,概念虽然容易,但是困难的工作是如果子类调用一个自身没有定义的属性,它是按照何种顺序去到父类寻找呢,尤其是众多父类中有多个都包含该同名属性。
Python 的类分为经典类与新式类。Python 2.7 之前的版本中可以采用经典类,经典类继承父类的顺序采用深度优先算法,但在 Python 3 之后的版本就只承认新式类了。新式类在 Python 2.2 之后的版本中都可以使用,新式类的继承顺序采用 C3 算法,其继承顺序可以通过查看 MRO 列表获取。经典类没有__MRO__
和instance.mro()
调用,而新式类有。
经典类中采用深度优先的匹配方法,可能导致在查询继承树中绕过后面的父类(在 Python 2 中测试,Python 默认使用新式类):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Python 2 class D(): def __init__(self): print("class D") class B(D): pass class C(D): def __init__(self): print("class C") class A(B, C): pass |
结果如下:
1 2 |
>>> f = A() class D |
新式类采用 C3 算法(区别于广度优先的原则)进行搜索,若使用新式类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Python 2 class D(object): def __init__(self): print("class D") class B(D): pass class C(D): def __init__(self): print("class C") class A(B, C): pass |
结果如下:
1 2 |
>>> f = A() class C |
这里为什么类 D 没有打印出来呢?因为类C跟类D有相同的方法,子类把父类给覆盖了。如果也想打印父类可以使用 super 方法(下面会介绍 super),如下:
1 2 3 4 |
class C(D): def __init__(self): print("class C") super(C, self).__init__() |
此时再去初始化类A,结果如下:
1 2 3 |
>>> A = A() class C class D |
经典类和新式类各自搜索的顺序如下图所示:
C3算法最早被提出是用于Lisp的,应用在Python中是为了解决原来基于深度优先搜索算法不满足本地优先级,和单调性的问题。
- 本地优先级:指声明时父类的顺序,比如A(B,C),如果访问A类对象属性时,应该根据声明顺序,优先查找B类,然后再查找C类。
- 单调性:如果在A的解析顺序中,B排在C的前面,那么在A的所有子类里,也必须满足这个顺序。
对于下面这一段程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class A(object): def __init__(self): print("A") class B(A): def __init__(self): print("B") class C(B): def __init__(self): print("C") class D(A): def __init__(self): print("D") class E(D): def __init__(self): print("E") class F(C, E): def __init__(self): print("F") |
当初始化实例F = F()时,使用深度优先搜索,广度优先搜索及C3算法的不同搜索顺序如下:
对于新式类,可以用instance.__mro__
或instance.mro()
来查看其MRO(Method Resolution Order 方法解析顺序)列表。对于上文代码中的类F
的MRO如下:
1 2 |
>>> print(F.mro()) [<class '__main__.F'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.D'>, <class '__main__.A'>, <class 'object'>] |
结果即C3算法的解析结果。C3线性化算法我们就不去深究了(太深入),感兴趣的读者可以自己去了解一下,总的来说,一个类的MRO列表就是合并所有父类的 MRO 列表,并遵循以下三条原则:
- 子类永远在父类前面。
- 如果有多个父类,会根据它们在列表中的顺序被检查。
- 如果对下一个类存在两个合法的选择,选择第一个父类。
同时为了解决多重继承中子类和父类有重复方法名的问题,Python 2.2之后引入了super函数。关于super,看Python类继承之super函数