习题 44: 继承(Inheritance) VS 合成(Composition)

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

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

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

这段故事就是为了教你避免使用“继承”这东西,这样说是不是更带感呢?有的程序员现在正在丛林里跟邪恶女皇作战,他会对你说你必须进到森林里去。他们这样说其实是因为他们需要你的帮助,因为他们已经无法承受他们自己创建的东西了。而对于你来说,你只要记住这一条:

大部分使用继承的场合都可以用合成取代,而多级继承则需要不惜一切地避免之。

什么是继承

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

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

  1. Actions on the child imply an action on the parent.
  2. Actions on the child override the action on the parent.
  3. Actions on the child alter the action on the parent.

I will now demonstrate each of these in order and show you code for them.

Implicit Inheritance

First I will show you the implicit actions that happen when you define a function in the parent, but not in the child.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Parent(object):

    def implicit(self):
        print "PARENT implicit()"

class Child(Parent):
    pass

dad = Parent()
son = Child()

dad.implicit()
son.implicit()

The use of pass under the class Child: is how you tell Python that you want an empty block. This creates a class named Child but says that there’s nothing new to define in it. Instead it will inherit all of its behavior from Parent. When you run this code you get the following:

PARENT implicit()
PARENT implicit()

Notice how even though I’m calling son.implicit() on line 16, and even though Child does not have a implicit function defined, it still works and it calls the one defined in Parent. This shows you that, if you put functions in a base class (i.e. Parent) then all subclasses (i.e. Child) will automatically get those features. Very handy for repetitive code you need in many classes.

Override Explicitly

The problem with implicitly having functions called is sometimes you want the child to behave differently. In this case you want to override the function in the child, effectively replacing the functionality. To do this just define a function with the same name in Child. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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()

In this example example I have a function named override in both classes, so let’s see what happens when you run it.

PARENT override()
CHILD override()

As you can see, when line 14 runs, it runs the Parent.override function because that variabe (dad) is a Parent. But, when line 15 runs it prints out the Child.override messages because son is an instance of Child and child overrides that function by defining it’s own version.

Take a break right now and try playing with these two concepts before continuing.

Alter Before Or After

The third way to use inheritance is a special case of overriding where you want to alter the behavior before or after you the parent class’s version runs. You first override the function just like in the last example, but then you use a Python built-in function named super to get the Parent version to call. Here’s the example of doing that so you can make sense of this description:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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()

The important lines here are 9-11, where in the child I do the following when son.altered() is called:

  1. Because I’ve overridden Parent.altered the Child.altered version runs, and line 9 executes like you’d expect.
  2. In this case I want to do a before and after so after line 9, I want to use super to get the Parent.altered version.
  3. On line 10 I call super(Child, self).altered(), which is a lot like the getattr function you’ve used in the past, but it’s aware of inheritance and will get the Parent class for you. You should be able to read this as “call super with arguments Child and self, then call the function altered on whatever it returns”.
  4. At this point, the Parent.altered version of the function runs, and that prints out the parent message.
  5. Finally, this returns from the Parent.altered and the Child.altered function continues to print out the after message.

If you then run this you should see this:

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

All Three Combined

To demonstrate all of these, I have a final version that shows each kind of interaction from inheritance in one file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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()

Go through each line of this code, and write a comment explaining what that line does and whether it’s an override or not. Then, run it and see that you get what you expected:

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

The Reason For super()

This should seem like commons sense, but then we get into trouble with a thing called Multiple Inheritance. Multiple Inheritance is when you define a class that inherits from one or more classes, like this:

class SuperFun(Child, BadStuff):
    pass

This is like saying, “Make a class named SuperFun that inherits from the classes Child and BadStuff at the same time.”

In this case, whenever you have implicit actions on any SuperFun instance, Python has to lookup the possible function in the class hierarchy for both Child and BadStuff, but it needs to do this in a consistent order. To do this Python uses something called “Method Resolution Order” (MRO) and an algorithm called C3 to get it straight.

Because the MRO is complex, and a well defined algorithm is used, Python can’t leave it to you to get it right. That’d be annoying wouldn’t it? Instead, Python gives you the super() function which handles all of this for you in the places that you need the altering type of actions as I did in Child.altered above. With super() you don’t have to worry about getting this right, and Python will find the right function for you.

Using super() With __init__

The most common use of super() is actually in __init__ functions in base classes. This is usually the only place where you need to do some things in a child, then complete the initialization in the parent. Here’s a quick example of doing that in the Child from these examples:

class Child(Parent):

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

This is pretty much the same as the Child.altered example above, except I’m setting some variables in the __init__ before having the Parent initialize with its Parent.__init__.

Composition

Inheritance is useful, but another way to do the exact same thing is just to use other classes and modules, rather than rely on implicit inheritance. If you look at the three ways to exploit inheritance, two of the three involve writing new code to replace or alter functionality. This can easily be replicated by just calling functions in a module. Here’s an example of doing this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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()"

son = Child()

son.implicit()
son.override()
son.altered()

In this code I’m not using the name Parent, since there is not a parent-child is-a relationship. This is a has-a relationship, where Child has-a Other that it uses to get its work done. When I run this I get the following output:

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

You can see that most of the code in Child and Other is the same to accomplish the same thing. The only difference is that I had to define a Child.implicit function to do that one action. I could then ask myself if I need this Other to be a class, and could I just make it into a module named other.py?

When To Use Inheritance Or Composition

The question of “inheritance vs. composition” comes down to an attempt to solve the problem of reusable code. You don’t want to have duplicated code all over your code, since that’s not clean and efficient. Inheritance solves this problem by creating a mechanism for you to have implied features in base classes. Composition solves this by giving you modules and the ability to simply call functions in other classes.

If both solutions solve the problem of reuse, then which one is appropriate in which situations? The answer is incredibly subjective, but I’ll give you my three guidelines for when to do which:

  1. Avoid multiple inheritance at all costs, as it’s too complex to be useful reliably. If you’re stuck with it, then be prepared to know the class hierarchy and spend time finding where everything is coming from.
  2. Use composition to package up code into modules that is used in many different unrelated places and situations.
  3. Use inheritance only when there are clearly related reusable pieces of code that fit under a single common concept, or if you have to because of something you’re using.

However, do not be a slave to these rules. The thing to remember about object oriented programming is that it is entirely a social convention programmers have created to package and share code. Because it’s a social convention, but one that’s codified in Python, you may be forced to avoid these rules because of the people you work with. In that case, find out how they use things and then just adapt to the situation.

Extra Credit

There is only one extra credit for this exercise because it is a big exercise. Go and read this http://www.python.org/dev/peps/pep-0008/ and start trying to use it in your code. You’ll notice that some of it is different from what you’ve been learning in this book, but now you should be able to understand their recommendations and use them in your own code. The rest of the code in this book may or may not follow these guidelines depending on if it makes the code more confusing. I suggest you also do this, as comprehension is more important than impressing everyone with you knowledge of esoteric style rules.

Frequently Asked Questions

How do I get better at solving problems that I haven’t seen before?

The only way to get better at solving problems is to solve as many problems as you can by yourself. Typically people hit a difficult problem and then rush out to find an answer. This is fine when you have to get things done, but if you have the time to solve it yourself, then take that time. Stop and bang your head against the problem for as long as possible, trying every possible thing, until you solve it or give up. After that the answers you find will be more satisfying and you’ll eventually get better at solving problems.

Aren’t objects just copies of classes?

In some languages (like JavaScript) that is true. These are called prototype languages and there’s not many differences between objects and classes other than usage. In Python however classes act as templates that “mint” new objects, similar to how coins were minted using a die (template).