你们仍是属肉体的,因为在你们中间有嫉妒分争,这岂不是属乎肉体,照着世人的样子行吗?...我栽种了,亚波罗浇灌了,惟有神叫他生长。(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

这就是典型的继承。这个类继承了objectobject是所有类的父类。这种定义是从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-8

class 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 b

class Girl(Person):
    def get_name(self):
        return self.name

if __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 sola
canglaoshi
{'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-8

class 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: ",n

class Girl(object):        #Python 3: class Gril:
    age = 28
    def color(self):
        print "The girl is white"

class HotGirl(Person, Girl):
    pass

if __name__ == "__main__":
    kong = HotGirl()
    kong.eye()
    kong.breast(90)
    kong.color()
    print kong.age

在这个程序中,前面有两个类PersonGirl,然后第三个类HotGirl继承了这两个类,注意观察继承方法,就是在类的名字后面的括号中把所继承的两个类的名字写上。但是第三个类中什么方法也没有。

然后实例化类HotGirl,既然继承了上面的两个类,那么那两个类的方法就都能够拿过来使用。保存程序,运行一下看看

$ python 20902.py 
two eyes
The breast is:  90
The girl is white
28

值得注意的是,这次在类Girl中,有一个age = 28,在对HotGirl实例化之后,因为继承的原因,这个类属性也被继承到HotGirl中,因此通过实例属性kong.age一样能够得到该数据。

由上述两个实例,已经清楚看到了继承的特点,即将父类的方法和属性全部承接到子类中;如果子类重写了父类的方法,就使用子类的该方法,父类的被遮盖。

多重继承的顺序很必要了解。

比如,如果一个子类继承了两个父类,并且两个父类有同样的方法或者属性,那么在实例化子类后,调用那个方法或属性,是属于哪个父类的呢?造一个没有实际意义,纯粹为了解决这个问题的程序:

#!/usr/bin/env python
# coding=utf-8

class 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):
    pass

class J2(K1, K2):
    def bar(self):
        print "J2-bar"

class C(J1, J2):
    pass

if __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-foo
J2-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)算法,读者对此若有兴趣,可以到网上搜索关于这个算法的内容进行了解。


总目录   |   上节:类(4)   |   下节:多态和封装

如果你认为有必要打赏我,请通过支付宝:[email protected],不胜感激。

results matching ""

    No results matching ""