zoukankan      html  css  js  c++  java
  • python 继承与派生

    继承的介绍

    继承是一种创建新类的方式,在Python中,新建的类可以继承一个或多个父类,新建的类可称为子类或派生类,父类又可称为基类或超类

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

    class ParentClass1: #定义父类
        pass
    
    class ParentClass2: #定义父类
        pass
    
    class SubClass1(ParentClass1): #单继承
        pass
    
    class SubClass2(ParentClass1,ParentClass2): #多继承
        pass

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

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

    在Python2中有经典类与新式类之分,没有显式地继承object类的类,以及该类的子类,都是经典类显式地继承object的类,以及该类的子类,都是新式类。而在Python3中,即使没有显式地继承object,也会默认继承该类,所以python3下的类都是新式类,如下

    >>> ParentClass1.__bases__
    (<class ‘object'>,)
    >>> ParentClass2.__bases__
    (<class 'object'>,)

    因而在Python3中统一都是新式类,关于经典类与新式类的区别

    提示:object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__

    python的多继承

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

    为何要用继承:用来解决类与类之间代码冗余问题

    继承与抽象

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

    基于抽象的结果,我们就找到了继承关系

     

    基于上图我们可以看出类与类之间的继承指的是什么’是’什么的关系(比如人类,猪类,猴类都是动物类)。子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。比如下面的定义Student类和Teacher类

    class Student:
        school="oldbly"
        def __init__(self,name,age,sex):
            self.name=name
            self.age=age
            self.sex=sex
    
        def choice_class(self):
            print("{} 正在选择班级".format(self.name))
    
    class Teacher:
        school = "oldbly"
        def __init__(self,name,age,sex,salary,level):
            self.name = name
            self.age = age
            self.sex = sex
            self.salary=salary
            self.level=level
    
        def teach(self):
            print('%s is teaching' %self.name)

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

    class OldboyPeople:
        school = "oldbly"
        def __init__(self, name, age, sex):
            self.name = name
            self.age = age
            self.sex = sex
    
    class Student(OldboyPeople):
        def choice_class(self):
            print("{} 正在选择班级".format(self.name))
    
    class Teacher(OldboyPeople):
    
        def __init__(self,name,age,sex,salary,level):
            # 指名道姓地跟父类OldboyPeople去要__init__
            OldboyPeople.__init__(self,name,age,sex,)
            self.salary=salary
            self.level=level
    
        def teach(self):
            print('%s is teaching' %self.name)
    
    stu1=Student("alex",18,"man")
    tea1=Teacher("egon",18,"man",20000,10)
    stu1.choice_class()
    tea1.teach()
    
    """
    执行结果
    alex 正在选择班级
    egon is teaching
    """

    Student类中并没有定义__init__方法,但是会从父类中找到__init__,因而仍然可以正常实例化,Teacher类内将与父类共有的__init__方法指明道姓从父类取,自己独有的__init__数据从自己这取,因而仍然可以正常实例化

    属性查找

    单继承背景下的属性查找

    对象在查找属性时,先从对象自己的__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是Bar的对象,obj.f2()带入的是Bar所以在父类查找到f2后执行self.f1()时时执行obj.f1()也就是先在Bar内查找
    Foo.f2 Bar.f1
    '''

    示例2:如果想在上例中执行self.f1()时先查找的就是Foo类内的f1可以用下面的方法

    方法一:

    class Foo:
        def f1(self):
            print('Foo.f1')
    
        def f2(self):
            print('Foo.f2')
            Foo.f1(self) # 调用当前类中的f1
    
    class Bar(Foo):
        def f1(self):
            print('Bar.f1')
    
    obj=Bar()
    obj.f2()
    
    '''
    执行结果
    Foo.f2
    Foo.f1
    '''

    方法二:

    class Foo:
        def __f1(self): # _Foo__f1
            print('Foo.f1')
    
        def f2(self):
            print('Foo.f2')
            self.__f1() # self._Foo__f1,#因为在定义阶段就发生了变形,so, self.__f1()==>self._Foo__f1(),so调用的是Foo类中的f1
    
    class Bar(Foo):
        def __f1(self): # _Bar__f1
            print('Bar.f1')
    
    obj=Bar()
    obj.f2()
    
    '''
    执行结果
    Foo.f2
    Foo.f1
    '''

    继承的原理和菱形问题

    菱形问题

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

    A类在顶部,B类和C类分别位于其下方,D类在底部将两者连接在一起形成菱形。

    这种继承结构下导致的问题称之为菱形问题:如果A中有一个方法,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的继承实现原理

    继承原理

    python到底是如何实现继承的呢? 对于你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表,如下

    >>> D.mro() # 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法
    [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

    python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。 而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:

    1.子类会先于父类被检查
    2.多个父类会根据它们在列表中的顺序被检查
    3.如果对下一个类存在两个合法的选择,选择第一个父类

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

    ps:

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

    深度优先与广度优先

    参照下述代码,多继承结构为非菱形结构,此时(不管是python3新式类,还是python2的经典类或新式类),都会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性

     

    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):
        # def test(self):
        #     print('from A')
        pass
    
    
    print(A.mro())
    '''
    [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>]
    '''
    
    obj = A()
    obj.test() # 结果为:from B
    # 可依次注释上述类中的方法test来进行验证

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

    class G: # 在python2中,未继承object的类及其子类,都是经典类
        def test(self):
            print('from G')
    
    class E(G):
        def test(self):
            print('from E')
    
    class F(G):
        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(G):
        def test(self):
            print('from D')
    
    class A(B,C,D):
        # def test(self):
        #     print('from A')
        pass
    
    obj = A()
    obj.test() # 如上图,查找顺序为:obj->A->B->E->G->C->F->D->object
    # 可依次注释上述类中的方法test来进行验证,注意请在python2.x中进行测试

    class G(object):
        def test(self):
            print('from G')
    
    class E(G):
        def test(self):
            print('from E')
    
    class F(G):
        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(G):
        def test(self):
            print('from D')
    
    class A(B,C,D):
        # def test(self):
        #     print('from A')
        pass
    
    obj = A()
    obj.test() # 如上图,查找顺序为:obj->A->B->E->C->F->D->G->object
    # 可依次注释上述类中的方法test来进行验证

     Python Mixins机制

    # 多继承的正确打开方式:mixins机制
    # mixins机制核心:就是在多继承背景下尽可能地提升多继承的可读性
    # ps:让多继承满足人的思维习惯=》什么"是"什么

    一个子类可以同时继承多个父类,这样的设计常被人诟病,一来它有可能导致可恶的菱形问题,二来在人的世界观里继承应该是个”is-a”关系。 比如轿车类之所以可以继承交通工具类,是因为基于人的世界观,我们可以说:轿车是一个(“is-a”)交通工具,而在人的世界观里,一个物品不可能是多种不同的东西,因此多重继承在人的世界观里是说不通的,它仅仅只是代码层面的逻辑。不过有没有这种情况,一个类的确是需要继承多个类呢?

    ​ 答案是有,我们还是拿交通工具来举例子:

    ​ 民航飞机、直升飞机、轿车都是一个(is-a)交通工具,前两者都有一个功能是飞行fly,但是轿车没有,所以如下所示我们把飞行功能放到交通工具这个父类中是不合理的

    class Vehicle:  # 交通工具
        def fly(self):
            '''
            飞行功能相应的代码        
            '''
            print("I am flying")
    
    class CivilAircraft(Vehicle):  # 民航飞机
        pass
    
    class Helicopter(Vehicle):  # 直升飞机
        pass
    
    class Car(Vehicle):  # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
        pass

    ​ 但是如果民航飞机和直升机都各自写自己的飞行fly方法,又违背了代码尽可能重用的原则(如果以后飞行工具越来越多,那会重复代码将会越来越多)。

    怎么办???为了尽可能地重用代码,那就只好在定义出一个飞行器的类,然后让民航飞机和直升飞机同时继承交通工具以及飞行器两个父类,这样就出现了多重继承。这时又违背了继承必须是”is-a”关系。这个难题该怎么解决?

    不同的语言给出了不同的方法,让我们先来了解Java的处理方法。Java提供了接口interface功能,来实现多重继承:

    // 抽象基类:交通工具类
    public abstract class Vehicle {
    }
    
    // 接口:飞行器
    public interface Flyable {
        public void fly();
    }
    
    // 类:实现了飞行器接口的类,在该类中实现具体的fly方法,这样下面民航飞机与直升飞机在实现fly时直接重用即可
    public class FlyableImpl implements Flyable {
        public void fly() {
            System.out.println("I am flying");
        }
    }
    
    
    
    // 民航飞机,继承自交通工具类,并实现了飞行器接口
    public class CivilAircraft extends Vehicle implements Flyable {
        private Flyable flyable;
    
        public CivilAircraft() {
            flyable = new FlyableImpl();
        }
    
        public void fly() {
            flyable.fly();
        }
    }
    
    // 直升飞机,继承自交通工具类,并实现了飞行器接口
    public class Helicopter extends Vehicle implements Flyable {
        private Flyable flyable;
    
        public Helicopter() {
            flyable = new FlyableImpl();
        }
    
        public void fly() {
            flyable.fly();
        }
    }
    
    // 汽车,继承自交通工具类,
    public class Car extends Vehicle {
    }
    java实现多重继承

    现在我们的飞机同时具有了交通工具及飞行器两种属性,而且我们不需要重写飞行器中的飞行方法,同时我们没有破坏单一继承的原则。飞机就是一种交通工具,可飞行的能力是飞机的属性,通过继承接口来获取。

    回到主题,Python语言可没有接口功能,但Python提供了Mixins机制,简单来说Mixins机制指的是子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(例如Mixin后缀),以此标识这些类只是用来混合功能的,并不是用来标识子类的从属"is-a"关系的,所以Mixins机制本质仍是多继承,但同样遵守”is-a”关系,如下

    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惯用的套路
    # 补充:通常Mixin结果的类放在左边

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

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

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

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

    class Displayer:
        def display(self, message):
            print(message)
    
    
    class LoggerMixin:
        def log(self, message, filename='logfile.txt'):
            with open(filename, 'a') as fh:
                fh.write(message)
    
        def display(self, message):
            super().display(message) # super的用法请参考下一小节
            self.log(message)
    
    
    class MySubClass(LoggerMixin, Displayer):
        def log(self, message):
            super().log(message, filename='subclasslog.txt') 
    
    
    obj = MySubClass()
    obj.display("This string will be shown and logged in subclasslog.txt")
    
    
    # 属性查找的发起者是obj,所以会参照类MySubClass的MRO来检索属性
    #[<class '__main__.MySubClass'>, <class '__main__.LoggerMixin'>, <class '__main__.Displayer'>, <class 'object'>]
    
    # 1、首先会去对象obj的类MySubClass找方法display,没有则去类LoggerMixin中找,找到开始执行代码
    # 2、执行LoggerMixin的第一行代码:执行super().display(message),参照MySubClass.mro(),super会去下一个类即类Displayer中找,找到display,开始执行代码,打印消息"This string will be shown and logged in subclasslog.txt"
    # 3、执行LoggerMixin的第二行代码:self.log(message),self是对象obj,即obj.log(message),属性查找的发起者为obj,所以会按照其类MySubClass.mro(),即MySubClass->LoggerMixin->Displayer->object的顺序查找,在MySubClass中找到方法log,开始执行super().log(message, filename='subclasslog.txt'),super会按照MySubClass.mro()查找下一个类,在类LoggerMixin中找到log方法开始执行,最终将日志写入文件subclasslog.txt
    View Code

    派生与方法重用

    子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找,例如每个老师还有职称这一属性,我们就需要在Teacher类中定义该类自己的__init__覆盖父类的

    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,title): # 派生
            self.name=name
            self.sex=sex
            self.age=age
            self.title=title
        def teach(self):
            print('%s is teaching' %self.name)
    
    obj=Teacher('lili','female',28,'高级讲师') #只会找自己类中的__init__,并不会自动调用父类的
    print(obj.name,obj.sex,obj.age,obj.title)   #lili female 28 高级讲师

    很明显子类Teacher中__init__内的前三行又是在写重复代码,若想在子类派生出的方法内重用父类的功能,有两种实现方式

    方法一:“指名道姓”地调用某一个类的函数==》不依赖于继承关系

    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,title): # 派生
            People.__init__(self,name,sex,age)    #调用的是函数,因而需要传入self,
            self.title=title
        def teach(self):
            print('%s is teaching' %self.name)
    
    obj=Teacher('lili','female',28,'高级讲师') 
    print(obj.name,obj.sex,obj.age,obj.title)   #lili female 28 高级讲师

    方法二:super()调用父类提供给自己的方法=》严格依赖继承关系

    调用super()会得到一个特殊的对象,该对象会参照发起属性查找的那个类的mro,去当前类的父类中找属性,

    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,title): # 派生
            #super(Teacher,self).__init__(name,sex,age)   #在Python2中super的使用需要完整地写成super(自己的类名,self) ,而在python3中可以简写为super()。
            super().__init__(name,sex,age)      #调用的是方法,自动传入对象、把super()放在子类派生的属性前面
            self.title=title
        def teach(self):
            print('%s is teaching' %self.name)
    
    obj=Teacher('lili','female',28,'高级讲师')
    print(obj.name,obj.sex,obj.age,obj.title)   #lili female 28 高级讲师

    这两种方式的区别是:方式一是跟继承没有关系的,而方式二的super()是依赖于继承的,并且即使没有直接继承关系,super()仍然会按照MRO继续往后查找

    案例:

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

    obj.test()首先找C自己下面有没有test()方法,发现没有再到A下查找test()方法,发现有test()方法,执行super().test()会基于MRO列表(以C.mro()为准)当前所处的位置(当前找到A,下一个是B)继续往后查找test(),然后在B中找到了test方法并执行。

    关于在子类中重用父类功能的这两种方式,使用任何一种都可以,但是在最新的代码中还是推荐使用super()

    组合

    在一个类中以另外一个类的对象作为数据属性,称为类的组合。组合与继承都是用来解决代码的重用性问题。不同的是:继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;而组合则是一种“有”的关系,比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合,如下示例

    class Course:
        def __init__(self,name,period,price):
            self.name=name
            self.period=period
            self.price=price
        def tell_info(self):
            print('<%s %s %s>' %(self.name,self.period,self.price))
    
    class Date:
        def __init__(self,year,mon,day):
            self.year=year
            self.mon=mon
            self.day=day
        def tell_birth(self):
           print('<%s-%s-%s>' %(self.year,self.mon,self.day))
    
    class People:
        school='清华大学'
        def __init__(self,name,sex,age):
            self.name=name
            self.sex=sex
            self.age=age
    
    #Teacher类基于继承来重用People的代码,基于组合来重用Date类和Course类的代码
    class Teacher(People): #老师是人
        def __init__(self,name,sex,age,title,year,mon,day):
            super().__init__(name,age,sex)
            self.birth=Date(year,mon,day) #老师有生日
            self.courses=[] #老师有课程,可以在实例化后,往该列表中添加Course类的对象
        def teach(self):
            print('%s is teaching' %self.name)
    
    
    python=Course('python','3mons',3000.0)
    linux=Course('linux','5mons',5000.0)
    teacher1=Teacher('lili','female',28,'博士生导师',1990,3,23)
    
    # teacher1有两门课程
    teacher1.courses.append(python)
    teacher1.courses.append(linux)
    
    # 重用Date类的功能
    teacher1.birth.tell_birth()
    
    # 重用Course类的功能
    for obj in teacher1.courses: 
        obj.tell_info()

    此时对象teacher1集对象独有的属性、Teacher类中的内容、Course类中的内容于一身(都可以访问到),是一个高度整合的产物

  • 相关阅读:
    .NET Interop 工具集
    关于正弦波的算法
    Windows Phone 系列 本地数据存储
    Xaml cannot create an instance of “X”
    Windows Phone 系列 使用 MVVM绑定时无法获取当前值
    Windows Phone 系列 应用程序图标无法显示
    Windows Phone 系列 WPConnect无法上网的问题
    Windows Phone 系列 使用 Windows Phone 保存铃声任务
    WP7.5提交应用
    Windows Phone 系列 动态删除ObservableCollection
  • 原文地址:https://www.cnblogs.com/baicai37/p/12667893.html
Copyright © 2011-2022 走看看