类(5)
你们仍是属肉体的,因为在你们中间有嫉妒分争,这岂不是属乎肉体,照着世人的样子行吗?...我栽种了,亚波罗浇灌了,惟有神叫他生长。(1 CORINTHIANS 3:3,6)
类(5)
继承
继承——OOP的三个特征:多态、继承、封装——是类的重要内容。
继承,也是人的贪欲。
在现实生活中,“继承”意味着一个人从另外一个人那里得到了一些什么,“继承”之后,自己就在所继承的方面省力气、不用劳神费心,能轻松得到。比如继承了万贯家产,就一夜之间变成富n代;如果继承了“革命先烈的光荣传统”,就红色?
当然,生活中的继承或许不那么严格,但是编程语言中的继承是有明确规定和稳定的预期结果的。
概念
继承(Inheritance)是面向对象软 件技术当中的一个概念。如果一个类别A“继承自”另一个类别B,就把这个A称为“B的子类别”,而把B称为“A的父类别”,也可以称“B是A的超类”。
继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。 (源自维基百科)
由上面对继承的表述,可以简单总结出继承的意图或者好处:
- 可以实现代码重用,但不是仅仅实现代码重用,有时候根本就没有重用
- 实现属性和方法继承
诚然,以上也不是全部,随着后续学习,对继承的认识会更深刻,例如网友令狐虫持有这样的观点:
从技术上说,OOP里,继承最主要的用途是实现多态。对于多态而言,重要的是接口继承性,属性和行为是否存在继承性,这是不一定的。事实上,大量工程实践表明,重度的行为继承会导致系统过度复杂和臃肿,反而会降低灵活性。因此现在比较提倡的是基于接口的轻度继承理念。这种模型里因为父类(接口类)完全没有代码,因此根本谈不上什么代码复用了。
在Python里,因为存在Duck Type,接口定义的重要性大大的降低,继承的作用也进一步的被削弱了。
另外,从逻辑上说,继承的目的也不是为了复用代码,而是为了理顺关系。
他是大牛,或许读者感觉比较高深,没关系,随着你的实践经验的积累,你也能对这个问题有自己独到的见解。
或许你也要问我的观点是什么?我的观点就是:走着瞧!怎么理解?继续向下看,只有你先深入这个问题,才能跳到更高层看这个问题。小马过河的故事还记得吧?只有亲自走入河水中,才知道河水的深浅。
在Python 2 中,我们这样定义新式类:
class NewStyle(object): pass
这就是典型的继承。这个类继承了object
,object
是所有类的父类。这种定义是从Python 2.2开始的,它解决了以往的类和类型的不统一的问题,自那以后,类就是一种数据类型了。
发展到Python 3,类的定义变为:
class NewStyle: pass
不再显示地写出object
,是因为Python 3中的所有类,都隐式地继承了object
。
总而言之,object
就是所有类的父类。
单继承
这是只从一个父类那里继承。
>>> class P(object): #Python 3: class P: pass>>> class C(P): pass
寥寥数“键”,就实现了继承。
类P
是一个通常的类,只不过在Python的两个版本中,定义样式稍微不同罢了。类C
(注意字母大写)则是定义的一个子类,它用C(P)
的形式继承了类P
——称之为父类——虽然父类什么也没有。
子类C
继承父类P
的方式就是在类名称后面的括号里面写上父类的名字,不管是Python的哪个版本。既然继承了父类,那么父类的一切都带入到了子类,所以在Python 2中就没有必要重复写object
了,它已经通过父类P
被继承到子类C
了;Python 3中,要显式的写上父类的名字,除了object
,它不会隐式继承任何其它类。
>>> C.__base__<class '__main__.P'>
还记得类的一个特殊属性吗?由C.__base__
可以得到类的父类。刚才的操作,就显示出类C
的父类是P
。
为了深入理解“继承”的作用,让父类做一点点事情。
>>> class P(object): #Python 3: class P: def __init__(self): print "I am a rich man." #Python 3: print("I am a rich man.")>>> class C(P): pass>>> c = C()I am a rich man.
父类P
中增加了初始化函数,然后子类C
继承它。我们已经熟知,当建立实例的时候,首先要执行类中的初始化函数。因为子类C
继承了父类,就把父类中的初始化函数拿到了子类里面,所以在c = C()
的时候,执行了父类中定义的初始化函数——这就是继承,而且是从一个父类那里继承来的,所以也称之为单继承。
看一个比较完成的程序示例。
#!/usr/bin/env python# coding=utf-8class Person(object): #Python 3: class Person: def __init__(self, name): self.name = name def height(self, m): h = dict((["height", m],)) return h def breast(self, n): b = dict((["breast", n],)) return bclass Girl(Person): def get_name(self): return self.nameif __name__ == "__main__": cang = Girl("canglaoshi") print cang.get_name() #Python 3: print(cang.get_name()),下同,从略 print cang.height(160) print cang.breast(90)
上面这个程序,保存之后运行:
canglaoshi{'height': 160}{'breast': 90}
对以上程序进行解释:
首先定义了一个类Person
,把它作为父类。然后定义了一个子类Girl
,继承了Person
。
在子类Girl
中,只写了一个方法get_name()
,但是因为是继承了Person
,那么Girl
就全部拥有了Person
中的方法和属性。子类Girl
的方法get_name()
中,使用了属性self.name
,但是在类Girl
中,并没有什么地方显示创建了这个属性,就是因为继承Person
类,在父类中有初始化函数。所以,当使用子类创建实例的时候,必须传一个参数cang = Girl("canglaoshi")
,然后调用实例方法cang.get_name()
。对于实例方法cang.height(160)
,也是因着继承的缘故使然。
在上面的程序中,子类Girl
里面没有与父类Person
重复的属性和方法,但有时候,会遇到这样的情况。
class Girl(Person): def __init__(self): self.name = "Aoi sola" def get_name(self): return self.name
在子类里面,也写了一个初始化函数,并且定义了一个实例属性self.name = "Aoi sola"
。在父类中,也有初始化函数。在这种情况下,再次执行程序。
在Python 2中出现异常:
TypeError: __init__() takes exactly 1 argument (2 given)
Python 3中也有异常:
TypeError: __init__() takes 1 positional argument but 2 were given
不管哪个版本中的异常信息,都告诉我们,创建实例的时候,传入的参数个数多了。根源在于,子类Girl
中的初始化函数,只有一个self
。因为跟父类中的初始化函数重名,虽然继承了父类,但是将父类中的初始化函数覆盖了,导致父类中的__init__()
在子类中不再实现。所以,实例化子类,不应该再显式地传参数。
if __name__ == "__main__": cang = Girl() #不在显示地传参数 print cang.get_name() #Python 3: print(cang.get_name()),下同,从略 print cang.height(160) print cang.breast(90)
如此修改之后,再运行,则显示结果:
Aoi sola{'height': 160}{'breast': 90}
从结果中不难看出,如果子类中的方法或属性覆盖了父类(即与父类同名),那么就不在继承父类的该方法或者属性。
像这样,子类Girl
里面有与父类Person
同样名称的方法和属性,也称之为对父类相应部分的重写。重写之后,父类的相应部分不再被继承到子类,没有重写的部分,在子类中依然被继承,从上面程序可以看出来此结果。
还有一种可能存在,就是重写之后,如果要在子类中继承父类中相应部分,怎么办?
调用覆盖的方法
承接前面的问题和程序,可以对子类Gril
做出这样的修改。
class Girl(Person): def __init__(self, name): Person.__init__(self, name) self.real_name = "Aoi sola" def get_name(self): return self.name
请读者注意观察Girl
的初始化方法,与前面的有所不同。为了能够使用父类的初始化方法,以类方法的方式调用Person.__init__(self, name)
。另外,在子类的__init__()
的参数中,要增加相应的参数name
。这样就回答了前面的问题。
实例化子类,以下面的方式运行程序:
if __name__ == "__main__": cang = Girl("canglaoshi") print cang.real_name print cang.get_name() print cang.height(160) print cang.breast(90)
执行结果为:
Aoi solacanglaoshi{'height': 160}{'breast': 90}
就这样,使用类方法的方式,将父类中被覆盖的方法再次在子类中实现。
但上述方式有一个问题,如果父类的名称因为某种目前你无法预料的原因修改了,子类中该父类的的名称也要修改,有如果程序比较复杂或者忘记了,就会出现异常。于是乎,就有了更巧妙的方法——super
。再重写子类。
class Girl(Person): def __init__(self, name): #Person.__init__(self, name) super(Girl, self).__init__(name) self.real_name = "Aoi sola" def get_name(self): return self.name
仅仅修改一处,将Person.__init__(self, name)
修改为super(Girl, self).__init__(name)
。执行程序后,显示的结果与以前一样。
关于super
,有人做了非常深入的研究,推荐读者阅读《Python’s super() considered super! 》,文中已经探究了super
的工作过程。读者如果要深入了解,可以阅读这篇文章。
多重继承
前面所说的继承,父类都只有一个。但,继承可以来自多个“父”,这就是多重继承。
所谓多重继承,就是指某一个子类的父类,不止一个,而是多个。比如:
#!/usr/bin/env python# coding=utf-8class Person(object): #Python 3: class Person: def eye(self): print "two eyes" #Python 3: print("two eyes"),下同,从略 def breast(self, n): print "The breast is: ",nclass Girl(object): #Python 3: class Gril: age = 28 def color(self): print "The girl is white"class HotGirl(Person, Girl): passif __name__ == "__main__": kong = HotGirl() kong.eye() kong.breast(90) kong.color() print kong.age
在这个程序中,前面有两个类Person
和Girl
,然后第三个类HotGirl
继承了这两个类,注意观察继承方法,就是在类的名字后面的括号中把所继承的两个类的名字写上。但是第三个类中什么方法也没有。
然后实例化类HotGirl
,既然继承了上面的两个类,那么那两个类的方法就都能够拿过来使用。保存程序,运行一下看看
$ python 20902.py two eyesThe breast is: 90The girl is white28
值得注意的是,这次在类Girl
中,有一个age = 28
,在对HotGirl实例化之后,因为继承的原因,这个类属性也被继承到HotGirl
中,因此通过实例属性kong.age
一样能够得到该数据。
由上述两个实例,已经清楚看到了继承的特点,即将父类的方法和属性全部承接到子类中;如果子类重写了父类的方法,就使用子类的该方法,父类的被遮盖。
多重继承的顺序很必要了解。
比如,如果一个子类继承了两个父类,并且两个父类有同样的方法或者属性,那么在实例化子类后,调用那个方法或属性,是属于哪个父类的呢?造一个没有实际意义,纯粹为了解决这个问题的程序:
#!/usr/bin/env python# coding=utf-8class K1(object): #Python 3: class K1: def foo(self): print "K1-foo" #Python 3: print("K1-foo"),下同,从略class K2(object): #Python 3: class K2: def foo(self): print "K2-foo" def bar(self): print "K2-bar"class J1(K1, K2): passclass J2(K1, K2): def bar(self): print "J2-bar"class C(J1, J2): passif __name__ == "__main__": print C.__mro__ m = C() m.foo() m.bar()
这段代码,保存后运行:
$ python 20904.py (<class '__main__.C'>, <class '__main__.J1'>, <class '__main__.J2'>, <class '__main__.K1'>, <class '__main__.K2'>, <type 'object'>)K1-fooJ2-bar
代码中的print C.__mro__
是要打印出类的继承顺序。从上面清晰看出来了。如果要执行foo()
方法,首先看J1
,没有,看J2
,还没有,看J1
里面的K1
,有了,即C==>J1==>J2==>K1;bar()
也是按照这个顺序,在J2
中就找到了一个。
这种对继承属性和方法搜索的顺序称之为“广度优先”。
Python 2的新式类,以及Python 3中都是按照此顺序原则搜寻属性和方法的。
但是,在旧式类中,是按照“深度优先”的顺序的。因为后面读者也基本不用旧式类,所以不举例。如果读者愿意,可以自己模仿上面代码,探索旧式类的“深度优先”含义。
导致新式类和Python 3中继承顺序较旧式类有所变化,其原因是mro(Method Resolution Order)算法,读者对此若有兴趣,可以到网上搜索关于这个算法的内容进行了解。