Python 21 面向对象编程-继承

Python 21 面向对象编程-继承

旧博客Python系列

组合

开始之前先讲一下组合,就是将不同的对象赋给其他对象的组成部分,即可构成一个大的组合,在面向对象编程中,大对象包含小对象是非常普遍的做法.对象和其被包含的对象通信也比较方便.
class Hand:
    pass


class Foot:
    pass


class Trunk:
    pass


class Head:
    pass


class Person:
    def __init__(self, id, name):
        self.id = id
        self.name = name
        self.hand = Hand()
        self.foot = Foot()
        self.trunk = Trunk()
        self.head = Head()


new_person = Person('1', 'cony')

for i in new_person.__dict__.items():
    print(i)
# ('id', '1')
# ('name', 'cony')
# ('hand', <__main__.Hand object at 0x000001E63933EC18>)
# ('foot', <__main__.Foot object at 0x000001E63933EC50>)
# ('trunk', <__main__.Trunk object at 0x000001E63933EC88>)
# ('head', <__main__.Head object at 0x000001E63933ECC0>)

类的继承

类的继承与现实生活的里的继承的基本概念一样,当然不是这么简单的理解.
# 继承,在定义类的时候,将父类(也成为基类)写在类名后边的括号里.
class ParentClass1:
    pass

class ParentClass2:
    pass

class SubClass1(ParentClass1): #单继承
    pass

class SubClass2(ParentClass1,ParentClass2): # 可以继承多个类,不只2个
    pass

子类继承了父类的什么内容呢?其实是所有可以继承的属性(功能)(肯定会有不可继承的内容).
和对象与类的关系类似,继承的各种数据属性和函数属性都可以改写,改写以后就属于子类,与父类独立,同名的也是这样.改写以后如果还是想调用父类方法怎么办,后文有说.

何时用继承
当类之间有显著的不同,并且较大的类的组件可以由较小的类的功能提供,组织代码的时候优先考虑组合.
当类之间有很多相同的功能的时候,提取这些共同的功能作为基类,然后用继承比较好,这样可以显著的减少代码量和耦合.

继承同时具有两种方式,有各自的意义:
方式1:子类直接继承基类,然后在基类的属性上做出改变或者扩展
意义2:几个类或者说功能模块,要实现相同的接口,没有必要为每个类编写接口;可以定义一个接口类,然后把几个类都继承那个接口类,即可实现统一的接口,后续也方便修改和维护.类似的场景在程序开发中非常多,父类一般是用于规定子类必须实现这些方法,但是父类不一定编写了方法,只是作为一个规定.用已经有的类建立一个新的类,这样就重用了已经有的软件中的一部分设置大部分,大大节省了编程工作量,这就是常说的软件重用,不仅可以重用自己的类,也可以继承别人的,比如标准库,来定制新的数据类型,这样就是大大缩短了软件开发周期,对大型软件开发来说,意义重大.

在实际开发的过程中,第一种方式的用处不大,因为子类会和父类耦合在一起,如果子类做出的修改不多,实际继承没有意义,还增加了耦合程度.实际开发过程中采用的,是第二种方式.

接口继承与归一化:抽象类

所谓接口继承,就是采取一种方法让类继承一个接口类,接口类里定义好了方法,来让各个对象拥有统一的方法. 如果在接口类里直接定义一个方法,对继承类是没有什么约束的(继承类里边没有写实现也可以),必须引入一种方法,对继承类进行强制规定,必须在继承类内独立写该方法,而不能自动继承接口类的方法. 这种方法就是引入abc库,然后使用metaclass以及类的装饰器@abc.abstractmethod.如果子类不实现,就无法创立对象.可见接口类的方法无需实现,只是用来规范子类.这种接口类有个名称叫做抽象类:Abstract Base Classes,ABC模块就是抽象类的简称. 例子如下:
import abc

class Dad(metaclass=abc.ABCMeta):   # metaclass用法,以后再学习
    '''This is the father class.'''

    money = 10

    def __init__(self, name):
        print(""Dad init is triggered"")  
        self.name = name

    @abc.abstractmethod         #这个装饰器就将这个方法定义为抽象类里的方法
    def hit_son(self):
        print('{} is beating his son'.format(self.name))

class Son(Dad):
    '''This is the son class'''
    pass

cony = Son('cony')  # 执行后报错TypeError: Can't instantiate abstract class Son with abstract methods hit_son

通过错误信息可以看到,无法通过Son类初始化对象,因为缺少hit_son方法,这就要求必须在Son类的代码里包含hit_son方法才行,这样抽象类就对继承类进行了规定.

继承的顺序

在多继承的情况下,如果子类没有某个属性,那么python是如何查找的呢? 假如有如下的类,继承关系是: G-->E-->C-->A G-->F-->D-->B-->A
class A:
    def tell(self):
        print('A')
class B(A):
    def tell(self):
        print('B')
class C(A):
    def tell(self):
        print('C')
class D(B):
    def tell(self):
        print('D')
class E(C):
    def tell(self):
        print('E')
class F(D):
    def tell(self):
        print('F')
class G(E,F):
    def tell(self):
        print('G')

通过反复删除子类的方法来运行,可以得到一个查找顺序:G-->E-->C-->F-->D-->B-->A.顺序可能比较奇怪,直接看原理:
新式类的继承顺序有一个固定的顺序,每定义一个类,会计算出一个MRO解析表(是一个元组),可以用classname.__mro__来获得该元组.(对象没有该属性).
寻找属性的顺序,就按照该元组从左到右的顺序进行搜索.

print(G.__mro__)
# (, , , , , , , )

可见顺序与实测顺序一致,最后的class 'object'是python内置的一切类的基类,只要定义了一个类,默认就会继承object类.这也就是为什么python3不再区分新式类和经典类的原因.

补充:python2的经典类如何继承
python2定义基类的时候如果不指定任何继承,则是经典类.经典类不存在__mro__属性
同样的方法测试可知道寻找的顺序是:G-->E-->C-->A-->F-->D-->B,可见是先深入到最深,再找其他分支.

子类中调用父类的方法

子类如果想调用父类的方法,最先想到的是在类内直接采用类名.方法的方式调用.

class Father:
    def __init__(self,name,age,hobby,gender):
        self.name = name
        self.age = age
        self.hobby = hobby
        self.gender = gender


class Daughter(Father):
    
    def __init__(self,name,age,hobby,gender,boyfriend):
        self.name = name
        self.age = age
        self.hobby = hobby
        self.gender = gender
        self.boyfriend = boyfriend

观察可以发现,Daughter类的初始化属性比Father类多了一个,之前的各个参数与Father类的逻辑相同,所以可以在代码里修改一下,对前四个参数使用Father的方法,只要针对最后一个新的参数写一行即可.

class Father:
    def __init__(self,name,age,hobby,gender):
        self.name = name
        self.age = age
        self.hobby = hobby
        self.gender = gender


class Daughter(Father):

    def __init__(self,name,age,hobby,gender,boyfriend):
        Father.__init__(self,name,age,hobby,gender)   # 调用父类的方法
        self.boyfriend = boyfriend

d = Daughter('cony',4,'play','female','unknown')

这种方式可以发现问题:
1 其实这个方法与对象的父类没有本质关系,通过类名.参数名的方法,可以调用任何一个可调用的类的方法,不限于父类,而且可扩展性很差,父类如果更改名称或者代码,子类的代码需要大幅修改,所以其实这种调用方法与继承毫无关系.
2 调用的函数其实就相当于普通的外部函数,第一个参数还是需要写self,否则会报错.

所以如果调用父类方法,简单的写类名.方法名并不是一个好的做法.正确调用父类方法的方式是采用super()表示父类来调用方法.上述的代码需要修改成:

class Father:
    def __init__(self,name,age,hobby,gender):
        self.name = name
        self.age = age
        self.hobby = hobby
        self.gender = gender

    def tell(self):
        print(""My name is {}\nMy age is {}\nI like {}\n"".format(self.name,self.age,self.hobby),end='')


class Daughter(Father):

    def __init__(self,name,age,hobby,gender,boyfriend):
        super().__init__(name,age,hobby,gender)
        self.boyfriend = boyfriend

    def tell(self):
        super().tell()
        print('My boyfriend is {}\n'.format(self.boyfriend),end='')

d = Daughter('cony',4,'play','female','unknown')
d.tell()

通过在python中查看内置的super(object)的解释: super() -> same as super(class, ).
所以super()就相当于找到当前类,然后传入self,然后去调用父类的方法,例子中子类的方法也可以写成:

    def __init__(self,name,age,hobby,gender,boyfriend):
        super(Daughter,self).__init__(name,age,hobby,gender)
        self.boyfriend = boyfriend

    def tell(self):
        super(Daughter,self).tell()
        print('My boyfriend is {}\n'.format(self.boyfriend),end='')
LICENSED UNDER CC BY-NC-SA 4.0
Comment