笨方法学Python 习题 44: 继承(Inheritance) VS 合成(Composition)

来源:互联网 发布:初学室内设计的软件 编辑:程序博客网 时间:2024/06/03 16:23

童话里经常会看到英雄打败恶人的故事,而且故事里总会有一个类似黑暗森林的场景——要么是一个山洞,要么是一篇森林,要么是另一个星球,反正是英雄不该去的某个地方。当然,一旦反面角色在剧情中出现,你就会发现英雄非得去那片破森林去杀掉坏人。当英雄的总是不得不冒着生命危险进到邪恶森

林中去。你很少会碰到这样的童话故事,说是英雄机智地躲过这些危险处境。你从不会听英雄说:“等等,如果我把白富美留在家里,自己跑出去当英雄闯世界,万一我半路死了,白富美就只好嫁给另一个矮穷挫王子。矮穷挫啊卧槽!我还是呆在这儿,做点出租童工的生意吧。”如果他选择了这条路,就不会碰到火、沼泽、死亡、复活、格斗、巨人,或者任何算得上故事的东西了。就是因为这个,这些故事里的森林就像黑洞一样,不管英雄是干嘛的,最终都无法避免地陷入其中。

在面向对象编程中,“继承”就是那片邪恶森林。有经验的程序员知道如何躲开这个恶魔,因为他们知道,在丛林深处的“继承”,其实是邪恶女皇“多重继承”。她喜欢用自己的巨口尖牙吃掉程序员和软件,咀嚼这些堕落者的血肉。不过这片丛林的吸引力是如此的强大,几乎每一个程序员都会进去探险,梦想着提着邪恶女皇的头颅走出丛林,从而声称自己是真正的程序员。你就是无法阻止丛林的魔力,于是你深入其中,而等你冒险结束,九死一生之后,你唯一学到的,就是远远躲开这片破森林,而如果你不得不再进去一次,你会带一支军队。这段故事就是为了教你避免使用“继承”这东西,这样说是不是更带感呢?有的程序员现在正在丛林里跟邪恶女皇作战,他会对你说你必须进到森林里去。他们这样说其实是因为他们需要你的帮助,因为他们已经无法承受他们自己创建的东西了。

而对于你来说,你只要记住这一条:大部分使用继承的场合都可以用合成取代,而多级继承则需要不惜一切地避免之。

什么是继承

继承的用处,就是用来指明一个类的大部分或全部功能,都是从一个父类中获得的。当你写 classFoo(Bar) 时,代码就发生了继承效果,这句代码的意思是“创建一个叫 Foo 的类,并让他继承“Bar” 。当你这样写时, Python 语言会让 Bar 的实例所具有的功能都工作在 Foo 的实例上。这样可以让你把通用的功能放到 Bar 里边,然后再给 Foo 特别设定一些功能。

当你这么做的时候,父类和子类有三种交互方式:

1. 子类上的动作完全等同于父类上的动作

2. 子类上的动作完全改写了父类上的动作

3. 子类上的动作部分变更了父类上的动作

我将通过代码向你一一展示。

隐 式继承(Implicit Inheritance )

首先我将向你展示当你在父类里定义了一个函数,但没有在子类中定义的例子,这时候会发生隐式继承。

class Parent(object):    def implicit(self):        print "PARENT implicit()"class Child(Parent):    passdad = Parent()son = Child()dad.implicit()son.implicit()

class Child: 中的 pass 是在 Python 中创建空的代码区块的方法。这样就创建了一个叫 Child 的类,但没有在里边定义任何细节。在这里它将会从它的父类中继承所有的行为。运行起来就是这样:

PARENT implicit()PARENT implicit()

就算我在第 16 行调用了 son.implicit() 而且就算 Child 中没有定义过 implicit 这个函数,这个函数依然可以工作,而且和在父类 Parent 中定义的行为一样。这就说明,如果你将函数放到基类中(也就是这里的 Parent ),那么所有的子类(也就是 Child 这样的类)将会自动获得这些函数功能。如果你需要很多类的时候,这样可以让你避免重复写很多代码。

显 式 覆 写(Explicit Override )

有时候你需要让子类里的函数有一个不同的行为,这种情况下隐式继承是做不到的,而你需要覆写子类

中的函数,从而实现它的新功能。你只要在子类 Child 中定义一个相同名称的函数就可以了,如下所示:

class Parent(object):    def override(self):        print "PARENT override()"class Child(Parent):    def override(self):        print "CHILD override()"dad = Parent()son = Child()dad.override()son.override()

这里我在两个类中都定义了一个叫 override 的函数,我们看看运行时会出现什么情况。

PARENT override()CHILD override()

如你所见,运行到第 14 行时,这里执行的是 Parent.override ,因为 dad 这个变量是定义在 Parent里的。不过到了第 15 行打印出来的却是 Child.override 里的信息,因为 son 是 Child 的一个实例,而子类中新定义的函数在这里取代了父类里的函数。现在来休息一下并巩固一下这两个概念,然后我们接着进行。

在运行前或运行后覆写

第三种继承的方法是一个覆写的特例,这种情况下,你想在父类中定义的内容运行之前或者之后再修改行为。首先你像上例一样覆写函数,不过接着你用 Python 的内置函数 super 来调用父类 Parent 里的版本。我们还是来看例子吧:

class Parent(object):    def altered(self):        print "PARENT altered()"class Child(Parent):    def altered(self):        print "CHILD, BEFORE PARENT altered()"        super(Child, self).altered()        print "CHILD, AFTER PARENT altered()"dad = Parent()son = Child()dad.altered()son.altered()

重要的是 9 到 11 行,当调用 son.altered() 时:

1. 由于我覆写了 Parent.altered ,实际运行的是 Child.altered ,所以第 9 行执行结果是预料之中的。

2. 这里我想在前面和后面加一个动作,所以,第 9 行之后,我要用 super 来获取 Parent.altered这个版本。

3. 第 10 行我调用了 super(Child, self).altered() ,这和你过去用过的 getattr 很相似,不过它还知道你的继承关系,并且会访问到 Parent 类。这句你可以读作“调用 super 并且加上 Child 和 self 这两个参数,在此返回的基础上然后调用 altered ” 。

4. 到这里 Parent.altered 就会被运行,而且打印出了父类里的信息。

5. 最后从 Parent.altered 返回到 Child.altered ,函数接着打印出来后面的信息。

运行的结果是这样的:

PARENT altered()CHILD, BEFORE PARENT altered()PARENT altered()CHILD, AFTER PARENT altered()

一 起使用三种方式

为了演示上面讲的内容,我来写一个最终版本,我们在一个文件中演示三种交互模式:

class Parent(object):    def override(self):        print "PARENT override()"    def implicit(self):        print "PARENT implicit()"    def altered(self):        print "PARENT altered()"class Child(Parent):    def override(self):        print "CHILD override()"    def altered(self):        print "CHILD, BEFORE PARENT altered()"        super(Child, self).altered()        print "CHILD, AFTER PARENT altered()"dad = Parent()son = Child()dad.implicit()son.implicit()dad.override()son.override()dad.altered()son.altered()

回到代码中,在每一行的上方写一个注解,写出它的功能,并且标出它是不是一个覆写动作。然后运行代码,看看输出的是不是你预期的内容:

PARENT implicit()PARENT implicit()PARENT override()CHILD override()PARENT altered()CHILD, BEFORE PARENT altered()PARENT altered()CHILD, AFTER PARENT altered()

为什么要用 super()

到这里也算是一切正常吧,不过接下来我们就要来应对一个叫多重继承( Multiple Inheritance )的麻烦东西。多重继承是指你定义的类继承了多个类,就像这样:

class SuperFun(Child, BadStuff):    pass

这相当于说“创建一个叫 SuperFun 的类,让它同时继承 Child 和 BadStuff ” 。这里一旦你在 SuperFun 的实例上调用任何隐式动作, Python 就必须回到类的层次结构中去检查Child 和 BadStuff ,而且必须要用固定的次序去检查。为实现这一点 Python 使用了一个叫 “方法解析顺序( Method Resolution Order , MRO )”的东西,还用了一个叫 C3 的算法。由于有这个复杂的 MRO 和这个很好的算法, Python 总不该把这些事情留给你去做吧,不然你不就跟着头大了?所以 Python 给你这个 super() 函数,用来在各种需要修改行为的场合为你处理,就像上面Child.altered 一样。有了 super() ,妈妈再也不用担心我吧继承关系弄糟,因为 Python 会给我找到正

确的函数。

super() 和 __init__ 搭配使用

最常见的 super() 的用法是在基类的 __init__ 函数中使用。通常这也是唯一可以进行这种操作的地方,在这里你在子类里做了一些事情,然后完成对父类的初始化。这里是一个在 Child 中完成上述行为的例子:

class Child(Parent):    def __init__(self, stuff):        self.stuff = stuff        super(Child, self).__init__()

这和上面的 Child.altered 差别不大,只不过我在 __init__ 里边先设了个变量,然后才用Parent.__init__ 初始化了 Parent 。

合成

继承是一种有用的技术,不过还有一种实现相同功能的方法,就是直接使用别的类和模块,而非依赖于继承。如果你回头看的话,我们有三种继承的方式,但有两种会通过新代码取代或者修改父类的功能。这其实可以很容易地用调用模块里的函数来实现。我们再来个例子:

class Other(object):    def override(self):        print("OTHER override()")    def implicit(self):        print("OTHER implicit()")    def altered(self):        print("OTHER altered()")class Child(object):    def __init__(self):        self.other = Other()    def implicit(self):        self.other.implicit()    def override(self):        print("CHILD override()")    def altered(self):        print("CHILD, BEFORE OTHER altered()")        self.other.altered()        print("CHILD, AFTER OTHER altered()")print("-*-"*30)son = Child()print("-*-"*30)son.implicit()print("-*-"*30)son.override()print("-*-"*30)son.altered()

这里我没有使用 Parent 这个名称,因为这里不是父类子类的“ A 是 B” 的关系,而是一个“ A 里有 B”的关系,这里 Child 里有一个 Other 用来完成它的功能。运行的时候,我们可以看到这样的输出:

OTHER implicit()CHILD override()CHILD, BEFORE OTHER altered()OTHER altered()CHILD, AFTER OTHER altered()
你可以看出, Child 和 Other 里的大部分内容是一样的,唯一不同的是我必须定义一个 Child.implicit函数来完成它的功能。然后我可以问自己,这个 Other 是写成一个类呢,还是直接做一个叫 other.py 的模块比较好?

继承和合成的应用场合

“继承 vs 合成”的问题说到底还是关于代码重用的问题。你不想到处都是重复的代码,这样既难看又没效率。继承可以让你在基类里隐含父类的功能,从而解决了这个问题。而合成则是利用模块和别的类中的函数调用实现了相同的目的。

如果两种方案都能解决重用的问题,那什么时候该用哪个呢?这个问题答案其实是非常主观的,不过我可以给你三个大体的指引方案:

1. 不惜一切代价地避免多重继承,它带来的麻烦比能解决的问题都多。如果你非要用,那你得准备

好专研类的层次结构,以及花时间去找各种东西的来龙去脉吧。

2. 如果你有一些代码会在不同位置和场合应用到,那就用合成来把它们做成模块。

3. 只有在代码之间有清楚的关联,可以通过一个单独的共性联系起来的时候使用继承,或者你受现有代码或者别的不可抗拒因素所限非用不可的话,那也用吧。

然而,不要成为这些规则的奴隶。面向对象编程中要记住的一点是,程序员创建软件包,共享代码,这些都是一种社交习俗。由于这是一种社交习俗,有时可能因为你的工作同事的原因,你需要打破这些规则。这时候,你就需要去观察别人的工作方式,然后去适应这种场合。

加分习题

本节只有一个加分习题,不过这个加分习题很大。去读一读 http://www.python.org/dev/peps/pep-0008/ 并在代码中应用它。你会发现其中有一些东西和本书中的不一样,不过你现在应该能懂得他们的推荐,并在自己的代码中应用这些规范。本书剩下的部分可能有一些没有完全遵循这些规范,不过这是因为有时候遵循规范反而让代码更难懂。我建议你也照做,因为对代码的理解比对风格规范的记忆更为重要。

常见问题回答

怎样增强自己解决新问题的技术?

提高解决问题能力的唯一方法就是自己去努力解决尽可能多的问题。很多时候人们碰到难题就会跑去找人给出答案。当你手头的事情非要完成不可的时候,这样做是没有问题的,不过如果你有时间自己解决的话,那就花时间去解决吧。停下手上的活,专注于你的问题死磕,试着用所有可能的方法去解决,不管最后解决与否都要试到山穷水尽为止。经过这样的过程,你找到的答案会让你更为满意,而你的解决问题的能力也提高了。

对象是不是就是类的拷贝?

有的语言里是这样的,例如 Javascript 。这样的语言叫做 prototype 语言,这种语言里的类和对象除了用法以外没多少不同。不过在 Python 里类其实像是用来创建对象的模板,就跟制作硬币用到的模具一样。