Python-面向对象编程之继承

一、基本概念

什么是继承

继承是一种创建新类的方式,新建的类可称为子类派生类被继承的类称为父类,父类又可称为基类或超类,子类会遗传父类的属性。

需要注意的是:python支持多继承,在Python中,新建的类可以继承一个或多个父类。

继承的形式:

class ParentClass1:  # 定义父类1
    pass

class ParentClass2:  # 定义父类2
    pass

class SubClass1(ParentClass1):  # 子类1 -> 单继承
    pass

class SubClass2(ParentClass1,ParentClass2):  #子类2 -> 多继承
    pass

通过类的内置属性__bases__可以查看类继承的所有父类

print(SubClass2.__bases__)
# (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)

PS: 在python2中有新式类经典类之分

新式类:继承了object类(python内置的一种类)的子类,以及该子类的子类子子类……
经典类:没有继承object类的子类,以及该子类的子类子子类……

在python3中没有继承任何类的类,那么会默认继承object类,所以python3中所有的类都是新式类

class Parent1:
    pass


class Parent2:
    pass


print(Parent1.__bases__)  # (<class 'object'>,)
print(Parent2.__bases__)  # (<class 'object'>,)

为何要用继承

用来解决类与类之间代码冗余问题

class ParentClass1:  # 定义父类
    x = 1


class ParentClass2:  # 定义父类
    y = 2


class SubClass(ParentClass1, ParentClass2):  # 多继承
    pass


obj = SubClass()
print(obj.x, obj.y)  # 1 2

注意:python的多继承

  1. 优点:子类可以同时遗传多个父类的属性,最大限度地重用代码
  2. 缺点:
  • 代码可读性会变差
  • 不建议使用多继承,有可能会引发可恶的菱形问题,扩展性变差。如果真的涉及到一个子类不可避免地要重用多个父类的属性,应该使用Mixins机制

二、继承的实现

要找出类与类之间的继承关系,需要先抽象,再继承。
抽象即总结相似之处,总结对象之间的相似之处得到类,总结类与类之间的相似之处就可以得到父类。

class Teacher:
    school = '无名大学'

    def __init__(self, name, sex, age, level):
        self.name = name
        self.sex = sex
        self.age = age
        self.level = level

    def info(self):
        print('教师姓名:%s|教师性别:%s|教师年龄:%s|教师职级:%s' % (self.name, self.sex, self.age, self.level))


class Studet:
    school = '无名大学'

    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

    def info(self):
        print('学生姓名:%s|学生性别:%s|学生年龄:%s' % (self.name, self.sex, self.age))

上述代码中,我们定义了同一所大学的教师类与学生类,并各自有输出基本信息功能。

类Teacher与Student之间存在重复的代码,老师与学生都是人类,所以我们可以得出如下继承关系,实现代码重用:

class People:
    school = '无名大学'

    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age


class Teacher(People):
    
    def __init__(self, name, sex, age, level):
        # 指名道姓地跟父类People去要__init__
        People.__init__(self, name, sex, age)
        self.level = level
        
    def info(self):
        print('教师姓名:%s|教师性别:%s|教师年龄:%s|教师职级:%s' % (self.name, self.sex, self.age, self.level))


class Studet(People):
    def info(self):
        print('学生姓名:%s|学生性别:%s|学生年龄:%s' % (self.name, self.sex, self.age))

Student类内并没有定义__init__方法,但是会从父类中找到__init__,因而仍然可以正常实例化。

Teacher类内比父类多了一个self.level属性,但其他三个属性是符合父类的。所以在Teacher类内还需要定义一个__init__方法,这个方法不仅先要指名道姓的去父类内要父类的__init__方法(相当于调用父类内的__init__方法),还要补充上父类没有的self.level属性。

stu_obj = Studet('山炮', '男', 18)
tea_obj = Teacher('李雷', '男', 29, '二级教师')

print(stu_obj.__dict__)  # {'name': '山炮', 'sex': '男', 'age': 18}
print(tea_obj.__dict__)  # {'name': '李雷', 'sex': '男', 'age': 29, 'level': '二级教师'}

stu_obj.info()  # 学生姓名:山炮|学生性别:男|学生年龄:18
tea_obj.info()  # 教师姓名:李雷|教师性别:男|教师年龄:29|教师职级:二级教师

三、单继承下的属性查找

有了继承关系,对象在查找属性时,先从对象自己的__dict__中找,如果没有则去实例化自己的子类中找,然后再去父类中找……

class Foo:
    def f1(self):
        print('Foo.f1')

    def f2(self):
        print('Foo.f2')
        self.f1()  # obj.f1()


class Bar(Foo):
    def f1(self):
        print('Bar.f1')


obj = Bar()
obj.f2()

# 运行结果:
# Foo.f2
# Foo.f1

obj.f2()会在父类Foo中找到f2,先打印Foo.f2,然后执行到self.f1(),即obj.f1()

在查找的过程中仍会按照:对象本身 -> 类Bar -> 父类Foo的顺序依次找下去,在类Bar中找到f1,因而打印结果为Foo.f1

如果是下面这种情况,因为隐藏属性在定义类时语法会发生不可见变化,所以也不会影响查找。

class Foo:
    def __f1(self):  # _Foo__f1
        print('Foo.f1')

    def f2(self):
        print('Foo.f2')
        self.__f1()  # self._Foo__f1  # 调用当前类中的f1


class Bar(Foo):
    def __f1(self):  # _Bar__f1
        print('Bar.f1')


obj = Bar()
obj.f2()

# 运行结果:
# Foo.f2
# Foo.f1

四、多继承下的菱形问题与MRO

(一)菱形问题

大多数面向对象语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的。

这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发菱形问题(或称钻石问题),菱形其实就是对下面这种继承结构的形象比喻。

python继承super__init__ 传参_父类

这种继承结构下导致的问题称之为菱形问题:即如果A(一定是非object类)中有一个方法,B和/或C都重写了该方法,而D没有重写它,那么D继承的是哪个版本的方法:B的还是C的?

代码则如下所示:

class A(object):
    def test(self):
        print('from A')


class B(A):
    def test(self):
        print('from B')


class C(A):
    def test(self):
        print('from C')


class D(B, C):
    pass


obj = D()
obj.test()  # 结果为:from B

要想搞明白obj.test()是如何找到方法test的,需要了解python的继承实现原理(MRO)

(二)MRO

Python到底是如何实现继承的呢?

对于我们定义的每一个类(不管有没有继承),Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表。

class A(object):
    def test(self):
        print('from A')

class B(A):
    def test(self):
        print('from B')

class C(A):
    def test(self):
        print('from C')

class D(B, C):
    pass

print(D.mro())
# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

类相关的属性查找(类名.属性该类的对象.属性),都是参照该类的mro列表顺序进行查找的。

比如类D以及类D的对象访问属性都是参照类D的mro列表类C以及类C的对象访问属性都是参照类C的mro列表

五、广度优先(不论版本)与深度优先(只存在于python2)

如果多继承是非菱形继承,继承结构如下图所示:

python继承super__init__ 传参_父类_02

此时,会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序进行查找,直到找到我们想要的属性。即一个分支一个分支地找到底,所有分支找完了最后找object(经典类与新式的属性查找顺序一样)。

与上图相对应的代码如下:

class E:
    def test(self):
        print('from E')


class F:
    def test(self):
        print('from F')


class B(E):
    def test(self):
        print('from B')


class C(F):
    def test(self):
        print('from C')


class D:
    def test(self):
        print('from D')


class A(B, C, D):
    pass


print(A.mro())
# [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>]
如果继承关系为菱形结构,继承结构如下图所示:

python继承super__init__ 传参_多继承_03

此时,会按照先找B这一条分支找到E停下不去找G,然后再找C这一条分支找到F停下不去找G,然后找D再去找G,最后找object的顺序进行查找,直到找到我们想要的属性。

与上图相对应的代码如下:

class G():
    def test(self):
        print('from G')


class E(G):
    pass


class F(G):
    def test(self):
        print('from F')


class B(E):
    pass


class C(F):
    def test(self):
        print('from C')


class D(G):
    def test(self):
        print('from D')


class A(B, C, D):
    pass


obj = A()
obj.test()  # from C
print(A.mro())
# [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class '__main__.G'>, <class 'object'>]
# 查找顺序为:obj->A->B->E->C->F->D->G->object

这种菱形结构下“查找不找底,最后才找底”的方式我们称为广度优先

有一种特殊情况,即在python2版本中,当多继承菱形结构的顶级父类是经典类时,查找顺序遵循的是深度优先

如下图所示:

python继承super__init__ 传参_多继承_04

这种“第一次就找到底,随后几次不找底”的方式我们称为深度优先。(只存在于python2中)

总结: 如果多继承是菱形继承,经典类与新式类的属性查找顺序不一样:
  • 经典类:深度优先,会在检索第一条分支的时候就直接一条道走到黑,即会检索大脑袋(共同的父类)
  • 新式类:广度优先,会在检索最后一条分支的时候检索大脑袋

经典类: 在python2中,未继承object的类及其子类,都是经典类。
新式类: 在python3中,所有的类都是新式类;在python2中,继承了object的类及其子类才是新式类。

'''
多继承到底要不用???
要用,但是规避几点问题

1、继承结构尽量不要过于复杂
2、推荐使用mixins机制
'''

六、多继承的正确打开方式:mixins规范

mixins规范核心:就是在多继承背景下尽可能地提升多继承的可读性

多继承是Python语言独有的特性, 一个子类可以同时继承多个父类。

我们拿食物来举一个例子:麻辣鸡丝、可口可乐、雪碧都属于是食物,后两者都有一个特性或者功能是味道甜,但是麻辣鸡丝是辣的,所以如下所示,我们把“甜”放到食物这个顶级父类中是极不合理的,容易引起歧义。

class food:  # 食物
    def 甜的(self):
        print("I am sweet")

class 雪碧(food):
    pass

class 可口可乐(food):
    pass

class 麻辣鸡丝(food):  # 麻辣鸡丝并不是甜的,是辣的,但按照这个继承关系,麻辣鸡丝也是甜的
    pass

但是如果可乐和雪碧都各自写自己的“甜的”方法,又违背了代码尽可能重用的原则(如果以后甜味的食物越来越多,那会重复代码将会越来越多)。怎么办?

为了尽可能地重用代码,那就只好再定义出一个“甜味”的类,然后让可乐和雪碧同时继承食物以及“甜味”两个父类,这样就出现了多重继承。当代码写的越来越多时,这种多重继承也将越来越多,父类与子类的从属关系将编织成一张大网,眼花缭乱。

所以Python提供了Mixins机制,简单来说Mixins机制指的是给子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(MixIn后缀),以此标识这些类只是用来混合功能的,并不是用来标识类间的从属关系(我虽然是继承了他的属性和功能,但我可不是他儿子啊,我俩没有血缘[从属]关系)。所以Mixins机制本质仍是多继承,但却是一种很好地解决上述问题的规范。

class food:  # 食物
    pass


class SweetMixIn:
    def 甜的(self):
        print("I am sweet")


class 雪碧(SweetMixIn, food):
    pass


class 可口可乐(SweetMixIn, food):
    pass


class 麻辣鸡丝(food):
    pass

使用Mixin类实现多重继承要非常小心

  • 首先它必须表示某一种功能,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
  • 其次它必须责任单一,一个类可以继承多个Mixin
  • 然后,它不依赖于子类的实现
  • 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如可乐照样可以吃(喝),就是不甜了)

Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向。

七、在子类派生的新方法中如何重用父类的功能

方式一:指名道姓调用某一个类下的函数 ==》不依赖于继承关系
class People:
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex

    def f1(self):
        print('%s say hello' % self.name)


class Teacher:
    def __init__(self, name, age, sex, level):
        People.__init__(self, name, age, sex)
        self.level = level


tea_obj = Teacher('张三', 28, '男', '二级教师')
print(tea_obj.__dict__)  # {'name': '张三', 'age': 28, 'sex': '男', 'level': '二级教师'}
方式二:super()调用父类提供给自己的方法 ==》严格依赖继承关系
class People:
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex

    def f1(self):
        print('%s say hello' % self.name)


class Teacher(People):
    def __init__(self, name, age, sex, level):
        super().__init__(name, age, sex)  # 调用的是方法,自动传入对象
        self.level = level


tea_obj = Teacher('张三', 28, '男', '二级教师')
print(tea_obj.__dict__)  # {'name': '张三', 'age': 28, 'sex': '男', 'level': '二级教师'}

调用super()会得到一个特殊的对象,该对象会参照发起属性查找的那个类mro,按照mro的顺序寻找属性。

super()案例
class A:
    def test(self):
        print('from A')
        super().test()


class B:
    def test(self):
        print('from B')


class C(A, B):
    pass


obj = C()
obj.test()

# 运行结果:
# from A
# from B

print(C.mro())  # [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

在上例中,发起属性查找的那个类是C,不是A;所以查找顺序是按照C的mro进行查找。