面向对象三大特性之继承

继承

继承是面向对象思想的另一个特性。它的存在是为了解决类与类之间代码重复的问题。

class Student:
    school_name = 'oldboy'
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex
      
    def select_course(self):
        print(f'学生:{self.name}正在选课。。。')

        
class Teacher:
    school_name = 'oldboy'
    def __init__(self, name, age, sex, level):
        self.name = name
        self.age = age
        self.sex = sex
        self.level = level
    
    def score(self):
        print(f'老师:{self.name}正在打分。。。')

上述两个类之间有大量重复的代码,我们现在介绍的这个继承就是为了解决这个代码重复的问题。

继承是一种创建新类的方式,如果基于一个已经存在的类A而创建了新类B。则新类B可称为子类或派生类,类A又可称为基类或超类,子类会遗传父类的属性。

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

class ParentClass1: #定义父类
    pass

class ParentClass2: #定义父类
    pass

class SubClass1(ParentClass1): #单继承
    pass

class SubClass2(ParentClass1,ParentClass2): #多继承
    pass
# python2中有经典类与新式类之分
	- 经典类:没有继承object类的子类,以及该子类的子类子子类。。。
	- 新式类:继承了object类的子类,以及该子类的子类子子类。。。
# 在python3中没有继承任何类,那么会默认继承object类,所以python3中所有的类都是新式类
# 提示:object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__

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

现实中是先有对象,再抽象出不同的类。如,人、动物。再在类的基础上继续抽象。

编程中则相反,先有父类再有子类。如果,类与类之间的关系为:什么'是'什么的关系(比如人类,猪类,猴类都是动物类)。则可以使用继承创建新类。子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。将学生类和老师类再抽象出一个人类,在人类里面写大家共有的代码,则每个子类内部写自己特有的代码,则解决了类与类之间代码重复的问题。

class OldBoyPeople(object):
    school = 'oldboy'
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex


class Student(OldBoyPeople):
    def select_course(self):
        print(f'学生:{self.name} 正在选课。。。')


class Teacher(OldBoyPeople):
    def __init__(self, name, age, sex, level):
        OldBoyPeople.__init__(self, name, age, sex) # '指名道姓'调用父类的这个方法,需要手动传第一个参数
        self.level = level

    def score(self):
        print(f'老师:{self.name} 正在打分。。。')

stu_obj = Student('lili', 18, 'female', 1001)	# Student类并没有__init__方法,会使用父类的
print(stu_obj.__dict__)

python的多继承

优点:子类可以同时遗传多个父类的属性,最大限度地重用代码
缺点:
	1、违背人的思维习惯:继承表达的是一种什么"是"什么的关系
	2、代码可读性会变差
	3、不建议使用多继承,有可能会引发可恶的菱形问题,扩展性变差,
如果真的涉及到一个子类不可避免地要重用多个父类的属性,应该使用Mixins

属性查找

本部分只谈单继承情况下的属性查找。下一部分详细谈多继承情况下的属性查找。

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

案例1:

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()

# 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()

继承的实现原理

菱形问题

大多数面向对象编程语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的。这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发著名的菱形问题(或称钻石问题,有时候也被称为“死亡钻石”)。菱形其实就是对下面这种继承结构的形象比喻。需要注意,这种结构中A不包括object类。

这种结构的继承关系将有可能导致菱形问题。要理解下面的执行结果,需要了解python的继承实现原理。

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

继承原理

这个继承原理其实就是在这种多继承的情况下,python到底按照什么原理来实现属性查找的。

# 你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有类的线性顺序列表
# 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法
D.mro() # 得到一个属性查找顺序列表
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Python会在MRO列表上从左到右开始查找,直到找到第一个匹配这个属性的类为止。 而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理。

所以obj.test()的查找顺序:先从对象obj本身的属性里找方法test,没有找到,则参照属性查找的发起者(即obj)所处类D的MRO列表来依次检索,首先在类D中未找到,然后再B中找到方法test

总结:从对象或者对象所在类的位置开始查找,找不到就从所在类在MRO列表中的位置往右找。。。

1.由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照对象的类.mro()规定的顺序依次找下去,
2.由类发起的属性查找,会按照当前类.mro()规定的顺序依次找下去

深度优先&广度优先

如果多继承结构为非菱形结构,此时,会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性。经典类和新式类的查找顺序相同,从左到右一条分支一条分支查。只不过新式类最后会多查找一个object类。

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

如果继承关系为菱形结构,那么经典类与新式类会有不同MRO,分别对应属性的两种查找方式:深度优先和广度优先

经典类的深度优先:查找顺序为,obj->A->B->E->G->C->F->D

新式类的广度优先:查找顺序为,obj->A->B->E->C->F->D->G->object


Mixins机制

多继承的正确打开方式:Mixins。Mixins核心机制:就是在多继承的背景下尽可能地提升多继承的可读性。通过命名规范来满足多继承满足人的思维习惯什么是什么的关系

class Vehicle:  # 交通工具
    pass

class FlyableMixin:
    def fly(self):
        print("I am flying")

class CivilAircraft(FlyableMixin, Vehicle):  # 民航飞机
    pass

class Helicopter(FlyableMixin, Vehicle):  # 直升飞机
    pass

class Car(Vehicle):  # 汽车
    pass

# ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路

可以看到,上面的CivilAircraft、Helicopter类实现了多继承,不过它继承的第一个类我们起名为FlyableMixin,而不是Flyable,这个并不影响功能。但是会告诉后来读代码的人,这个类是一个Mixin类,表示混入(mix-in),这种命名方式就是用来明确地告诉别人(python语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类。所以从含义上理解,CivilAircraft、Helicopter类都只是一个Vehicle,而不是一个飞行器。

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

  • 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀。
  • 其次它必须责任单一,如果有多个功能,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类。
  • 它不依赖于子类的实现;子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。
  • 通常Mixin的类放在子类括号内的左边。

Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性也是会越来越差的。所以,这种机制其实只是在一定程度上,避免程序因为使用多继承导致的可读性变差。因此,对于多继承的使用不推荐继承的结构过于复杂。


派生&方法重用

子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找。

如果,子类的某一个属性和父类的属性完全一样,则会覆盖父类的属性,优先使用自己的。

如果,子类的想使用父类的某个属性,但又是在父类的基础上扩展了新功能,则派生父类的方法。

派生的方式有两种:第一种为指名道姓的调用父类的属性;第二种为super()的调用方式

第一种和继承没有任何关系,只是函数的普通调用而已。

第二种是按照继承关系调用的。遇到super()属性,就会按照属性查找的发起者的MRO列表里面的下一个类里面找需要查找的属性。

>>> # A没有继承B
... class A:
...     def test(self):
...         super().test()
... 
>>> class B:
...     def test(self):
...         print('from B')
... 
>>> class C(A,B):
...     pass
... 
>>> C.mro() # 在代码层面A并不是B的子类,但从MRO列表来看,属性查找时,就是按照顺序C->A->B->object,B就相当于A的“父类”
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,<class ‘object'>]
>>> obj=C()
>>> obj.test() # 属性查找的发起者是类C的对象obj,所以中途发生的属性查找都是参照C.mro()
from B

补充:super()是给新式类使用的。python2中使用:super(类名, self).属性;python3可以简化为super().属性

>>> class Teacher(People):
...     def __init__(self,name,sex,age,title):
...         super().__init__(name,age,sex) 
		   # 调用的是绑定方法,自动传入self,python2:super(Teacher, self).__init__(name, age, sex)
...         self.title=title
...     def teach(self):
...         print('%s is teaching' %self.name)
...

组合

在一个类中以另外一个类的对象作为数据属性,称为类的组合。

组合与继承都是用来解决代码的重用性问题。

继承是一种“是”的关系,比如老师是人、学生是人。

组合则是一种“有”的关系,比如老师有生日,老师有多门课程。

class Student:
    def __init__(name, age):
        self.name = name
        self.age = age

class Coursr:
    pass

stu_obj = Student('jack', 18)
course_obj = Course()
stu_obj.course = course_obj
posted @ 2020-04-09 16:06  the3times  阅读(201)  评论(0编辑  收藏  举报