组合
开始之前先讲一下组合,就是将不同的对象赋给其他对象的组成部分,即可构成一个大的组合,在面向对象编程中,大对象包含小对象是非常普遍的做法.对象和其被包含的对象通信也比较方便.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-->Aclass 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='')