2015/9/22 Python基础(18):组合、派生和继承
一个类被定义后,目标就是把它当成一个模块来使用,并把这些对象嵌入到你的代码中去,同其他数据类型及逻辑执行流混合使用。
有两种方法可以在你的代码中利用类。
第一种是组合,就是让不同的类混合并加入到其他类中,来增强功能和代码重用性。你可以在一个大点的类中创建你自己的类的实例,实现一些其他属性和方法来增强原来的类对象。
另一种是派生,通过子类从基类继承核心属性,不断地派生扩展功能实现。
组合
举例来说,我们想对之前做过的地址本类作加强性设计。如果在设计的过程中,为names、addresses等创建了单独的类,那么最后我们可能想把这些工作集成到AddrBookEntry类中去,而不是重新设计每一个需要的类。
下面是一个例子
class NewAddrBookEntry(object): 'new address book entry class' def __init__ (self, nm, ph): self.name = Name(nm) self.phone = Phone(ph) print 'Created instance for:', self.name
NewAddrBookEntry 类由它自身和其他类组合而成。这就在一个类和其他组成类之间定义了一个“has-a”关系。比如说,我们的NewAddrBookEntry类“有一个”Name类实例和一个Phone实例。
创建复合对象就可以实现这些附加的功能,并且很有意义,因为这些类都不相同。每一个类管理他们自己的名字空间和行为。不过当对象之间有更接近的关系是,派生的概念可能更有意义。
这里没有写清楚如何使用组合这个概念,我就上面的部分写一个例子:
>>> class Name(object): def __init__(self, nm): self.name = nm >>> class Phone(object): def __init__(self, ph): self.phone = ph >>> class NewAddrBookEntry(object): def __init__(self, nm, ph): self.name = Name(nm) self.phone = Phone(ph) print 'Created instance for:', self.name print 'Created instance for:', self.name.name >>> foo = NewAddrBookEntry('Paul', 123456) Created instance for: <__main__.Name object at 0x02B75FD0> Created instance for: Paul
子类和派生
当类和类之间有显著的不同,并且(较小的类)是较大的类所需要的组件时,组合表现的不错,但如果要设计“相同的类但由一些不同的功能”时,派生就是更合理的选择了。
OOP的更强大的功能之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响系统中使用现存类的其他代码片段。
OOD允许类特征在子孙类或子类中进行继承。这些子类从基类(或称祖先类、超类)继承他们的核心属性。而且,这些派生可能会扩展到多代。在一个层次的派生关系中的相关类是父类和子类的关系,同一个父类派生出来的这些类是同胞关系。父类和所有高层类都被认为是祖先。
创建子类
创建子类的语法看起来与普通类没有区别,一个类名,后跟一个或多个需要从其中派生的父类:
class SubClassName (ParentClass1[, ParentClass2...): 'optional class documentation string' class_suite
如果你的类没有从任何祖先类派生,可以使用object作为父类名字。这也就是新式类的创建。
下面是子类派生的一个例子:
>>> class Parent(object): def parentMethod(self): print 'calling parent method' >>> class Child(Parent): def childMethod(self): print 'calling child method' >>> p = Parent() >>> p.parentMethod() calling parent method >>> c = Child() >>> c.childMethod() calling child method >>> c.parentMethod() calling parent method
继承
继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的积累的任何属性,不管是数据属性还是方法。
举例如下,P是一个没有属性的简单类,C从P继承而来,也没有属性:
>>> class P(object): pass >>> class C(P): pass >>> c = C() >>> c.__class__ <class '__main__.C'> >>> P.add = 123 >>> c.add 123
我们给父类添加数据属性时,子类也继承到了这个属性。
假如P添加一些属性:
>>> class P: 'P class' def __init__(self): print 'created an instance of',\ self.__class__.__name__ >>> class C(P): pass >>> p = P() created an instance of P >>> a = P() created an instance of P >>> p.__class__ <class __main__.P at 0x02A10490> >>> P.__bases__ () >>> P.__doc__ 'P class' >>> c = C() created an instance of C >>> c.__class__ <class __main__.C at 0x02BA8880> >>> C.__bases__ (<class __main__.P at 0x02A10490>,) >>> C.__doc__ >>>
C没有声明__init__()方法,然而在类C的实例c被创建时,还是会有输出星系。原因在于C集成了P的__init__()。__bases__元组列出父类P。需要的是文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性__doc__不会从基类中继承过来。
__bases__类属性
__bases__类属性是一个包含其父类的集合的元组。这里的父类是相对所有基类而言的。那些没有父类的类,__bases__属性为空。
>>> class A(object): pass >>> class B(A): pass >>> class C(B): pass >>> class D(B, A): pass >>> A.__bases__ (<type 'object'>,) >>> C.__bases__ (<class '__main__.B'>,) >>> D.__bases__ (<class '__main__.B'>, <class '__main__.A'>)
对于D,继承方向,我们将在之后详细讲解。
通过继承覆盖方法
我们在P中再写一个函数,然后在其子类中对它进行覆盖。
>>> class P(object): def foo(self): print 'call P-foo()' >>> p = P() >>> p.foo() call P-foo() >>> class C(P): def foo(self): print 'call C-foo()' >>> c = C() >>> c.foo() call C-foo()
尽管C继承了P的方法,但因为C定义了自己的foo()方法,所以P中的foo()方法被覆盖。
那我们怎么去调用那个别覆盖的基类的方法呢?
有这样几种方法:
>>> P.foo(c)
call P-foo()
这里我们没有用P的实例调用方法,而是用了P的子类C的实例c来调用。一般我们不会用这种方法调用。一般如下:
>>> class C(P): def foo(self): P.foo(self) print 'call C-foo()' >>> c = C() >>> c.foo() call P-foo() call C-foo()
这种方法需要我们知道C的父类,还有一个更好的方法是用super()内建函数
>>> class C(P): def foo(self): super(C, self).foo() print 'call C-foo()' >>> c = C() >>> c.foo()
核心笔记:重写__init__不会自动调用基类的__init__
类似于上面的覆盖非特殊方法,当从一个带构造器__init__()的类派生,如果你不去覆盖__init__(),它将会被继承并自动调用。但如果你在子类中覆盖了__init__(),子类被实例化时,基类的__init__()就不会被自动调用。如果还想调用基类的__init__(),需要向上边我们说的那样,明确指出,使用一个子类的实例去调用基类(未绑定)方法:
class C(P): def __init__(self): P.__init__(self) C__init__suite
这是一种很普遍的调用做法,这个规则的意义是,你希望被继承的类的对象在子类构造器运行前能够很好地被初始化或做好准备工作,因为它可能需要或设置继承属性。
当然我们也可以用super()内建函数替代P的使用,这样可以不提供基类的名字。
从标准类型派生
经典类中,一个最大的问题是,不能对标准类型进行子类化。后来随着类型和类的统一和新式类的引入,这一点已经被修正。
1.不可变类型的例子
金融应用中处理一个浮点数的子类,每次你得到一个货币值,你都需要四舍五入,变为带两位小数位的数值。你的类可以这样写:
class RoundFloat(float): def __new__(cls, val): return float.__new__(cls, round(val, 2))
我们了__new__()特殊方法来定制我们的对象,使之和标准Python浮点数有一些区别:我们使用了round()内建函数对元浮点数进行舍入操作,然后实例化我们的float, RoundFloat。我们是通过调用父类的构造器来创建真实的对象的,float.__new__()。注意所有的__new__()方法都是类方法,我们要显式传入类作为第一个参数。
以下是一些样例输出
>>> RoundFloat(1.5987) 1.6 >>> RoundFloat(0.4567) 0.46 >>> RoundFloat(-1.2334) -1.23
2.可变类型的例子
子类化一个可变类型于此相似,你可能不需要使用__new__()(甚至不用__init__()),因为通常设置不多。一般情况下,所继承到的类型默认行为就是你想要的。下例是一个字典类型:
class SortedKeyDict(dict): def keys(self): return sorted(super(SortedKeyDict, self).keys())
下面是使用新字典的例子:
>>> d = SortedKeyDict({'Anna':68, 'John':86,'Frank':78,'Cindy':88}) >>> print 'By iterator:',[key for key in d] By iterator: ['Frank', 'John', 'Anna', 'Cindy'] >>> print 'By keys():',d.keys() By keys(): ['Anna', 'Cindy', 'Frank', 'John']
当然,这种类方法调用有点多此一举,不如这样:
def keys(self): return sorted(self.keys())
多重继承
Python也允许子类继承多个基类。这种特性就是通常所说的多重继承。使用多重继承时,有两个不同的方面要记住,一是要找到合适的属性,二是重写方法时,如何调用父类方法让它们发挥作用,同时子类处理好自己的义务。
1.方法解释顺序(MRO)
在Python2.2以前的版本中,算法非常简单:深度优先,从左至右进行搜索,取得在子类中使用属性。其他Python算法只是覆盖被找到的名字,多重继承则取找到的第一个名字。
而现在的Python应用了C3算法
C3最早被提出是用于Lisp的,应用在Python中时为了解决深度优先搜索算法不满足本地优先级,和单调性问题。
本地优先级:指声明时父类的顺序,C(A,B),如果访问C类对象属性时,应该根据声明顺序,优先查找A类,然后查找B类。
单调性:如果在C的解析顺序中,A排在B的前面,那么在C的所有子类里,也必须满足这个顺序。
在Python官网中MRO的作者举了例
F=type('Food', (), {remember2buy:'spam'}) E=type('Eggs', (F,), {remember2buy:'eggs'}) G=type('GoodFood', (F,E), {})
根据本地优先级在调用G类对象属性时应该优先查找F类,而在Python2.3之前的算法给出的顺序是G E F O,而在心得C3算法中通过阻止类层次不清晰的声明来解决这一问题,以上声明在C3算法中就是非法的。
C3算法
判断mro要先确定一个线性序列,然后查找路径由序列中类的顺序决定,所以C3算法就是生成一个线性序列。
如果继承至一个基类:
class B(A)
这时B的mro序列为[B,A]
如果继承至多个基类
class B(A1,A2,A3 ...)
这时B的mro序列 mro(B) = [B] + merge(mro(A1), mro(A2), mro(A3) ..., [A1,A2,A3])
merge操作就是C3算法的核心。
遍历执行merge操作的序列,如果一个序列的第一个元素,在其他序列中也是第一个元素,或不在其他序列出现,则从所有执行merge操作序列中删除这个元素,合并到当前的mro中。
merge操作后的序列,继续执行merge操作,直到merge操作的序列为空。
如果merge操作的序列无法为空,则说明不合法。
例子:
class A(O):pass class B(O):pass class C(O):pass class E(A,B):pass class F(B,C):pass class G(E,F):pass
A、B、C都继承至一个基类,所以mro序列依次为[A,O]、[B,O]、[C,O]
mro(E) = [E] + merge(mro(A), mro(B), [A,B])
= [E] + merge([A,O], [B,O], [A,B])
执行merge操作的序列为[A,O]、[B,O]、[A,B]
A是序列[A,O]中的第一个元素,在序列[B,O]中不出现,在序列[A,B]中也是第一个元素,所以从执行merge操作的序列([A,O]、[B,O]、[A,B])中删除A,合并到当前mro,[E]中。
mro(E) = [E,A] + merge([O], [B,O], [B])
再执行merge操作,O是序列[O]中的第一个元素,但O在序列[B,O]中出现并且不是其中第一个元素。继续查看[B,O]的第一个元素B,B满足条件,所以从执行merge操作的序列中删除B,合并到[E, A]中。
mro(E) = [E,A,B] + merge([O], [O])
= [E,A,B,O]
2.简单属性查找示例
下面这个例子将对两种类的方案不同处做一展示。脚本由一组父类,一组子类,还有一个子孙类组成。
首先是经典类:
>>> class P1: #(object): def foo(self): print 'called P1-foo()' >>> class P2: #(object): def foo(self): print 'called P2-foo()' >>> class P2(object): def foo(self): print 'called P2-foo()' def bar(self): print 'called P2-bar()' >>> class C1(P1,P2): pass >>> class C2(P1, P2): def bar(self): print 'called C2-bar()' >>> class GC(C1, C2): pass
在经典类中,我们如下执行:
>>> gc = GC() >>> gc.foo() # GC => C1 =>P1 called P1-foo() >>> gc.bar() # GC => C1 => P1 => P2 called P2-bar()
在这种搜索方式下,foo()是很容易被理解的,而bar()的寻找则是通过GC,C1,P1后,找不到,就到P2中找到,C2根本不会被检索。
新式类,也就是在声明时加上(object)
结果如下:
>>> gc = GC() >>> gc.foo() # GC => C1 => C2 => P1 called P1-foo() >>> gc.bar() # GC => C1 => C2 called C2-bar()
新式类采用了一种广度优先的方式,查找顺序如图所示。同时,新式类有一个__mro__属性,可以告诉你查找顺序。
>>> GC.__mro__ (<class '__main__.GC'>, <class '__main__.C1'>, <class '__main__.C2'>, <class '__main__.P1'>, <class '__main__.P2'>, <type 'object'>)
为什么新式类的MRO方式和经典类出现了不同呢?这是因为菱形效应,如果继续沿袭经典类的深度优先搜索,可能会导致不能有效继承。
关于多重继承的顺序,我暂时也不是很清楚,等之后搞明白了这件事再写吧。
类、实例和其他对象的内建函数
issubclass()
这是布尔函数,判断一个类是不是另一个类的子类或者子孙类,语法如下:
issubclass(sub, sup)
True则是sub是sup的不严格子类,False则代表sub不是sup的子类。
isinstance()
这个布尔函数判定一个对象是否是另一个给定类的实例。语法如下:
isinstance(obj1, obj2)
isinstance()在obj1是obj2的一个实例或者是obj2子类的一个实例时,返回一个True。这里第二个参数必须是类,不然会得到TypeError.如果第二个参数是类型对象,这是允许的,我们也常常这样用它:
>>> isinstance(3, int) True >>> isinstance(2.2, int) False
hasattr()、getattr()、setattr()、delattr()
这些函数可以在各种对象下工作,不只是类和实例。
hasattr()函数是布尔型的,它的目的是为了决定一个对象是否有一个特定的属性,一般用于访问某些属性前做一下检查。getattr()和setattr()函数相应地取得和赋值给对象的属性,getattr()在你试图读取一个不存在的属性时,会引发AttributeError异常。setattr()将要加入一个新的属性,要么取代一个已存在的属性。而delattr()函数会从一个对象中删除属性。
dir()和super()
这两个内建函数之前都已经提到,在此不多加说明。
vars()
该内建函数与dir()相似,只是给定的对象参数必须有一个__dict__属性。vars()返回一个字典,它包含了对象存储于其__dict__中的属性(键)和值。