2.22. 継承

継承により、あるクラスが別のクラスを 拡張 できます -- そのすべてのメソッドと属性から始めて、異なる部分を追加またはオーバーライドします。元のクラスは 基底(または )と呼ばれ、拡張したものは サブクラス と呼ばれます。

基底クラス Shape がサブクラス Square を下向きに指しており、 Square は 1 つのメソッド(area)をオーバーライドし、 残りを継承している様子。

SquareShape のすべてのメソッドを再利用し、特殊化が必要なものをオーバーライドします。

2.22.1. サブクラスを定義する

基底クラスは class 行の括弧の中に入れます。

class Shape:
    def __init__(self, name):
        self.name = name

    def describe(self):
        return self.name + " with area " + str(self.area())

    def area(self):
        return 0

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def area(self):
        return self.side * self.side

s = Square(4)
print(s.describe())

出力:

square with area 16

SquareShape から describe を変更せずに継承し、実際の値を返すように area をオーバーライドします。基底の describeself.area() を呼び出すため依然として機能します -- これは実行時にオーバーライドへと解決されます。

2.22.2. super()

super() は、メソッドが基底クラス版のメソッドを呼び出せるようにするプロキシを返します。最も一般的な使い方は、サブクラスの __init__ から基底の __init__ を呼び出して、基底が自身の属性を初期化できるようにすることです。

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

super().__init__ を忘れることは、よくあるバグの原因です。基底が設定するはずだった属性が決して設定されず、それらに依存する継承されたメソッドが後でクラッシュします。

2.22.2.1. 暗黙の基底クラス

すべてのクラスは、Python の型階層の根である object を暗黙的に継承します。__repr____eq__ のようなメソッドや、属性アクセスの背後にある仕組みはすべて、クラスが基底を宣言していない場合でもそこから来ます。class Foo(object): と明示的に書くことと class Foo: と書くことは、現代の Python では同等です。後者が慣例的な形式です。

2.22.3. 継承が役立つとき -- そして役立たないとき

あるクラスが別のクラスの本当に より具体的な種類 であり、ほとんどの振る舞いを共有している場合に継承を使ってください。古典的なテストは「is-a(〜である)」の確認です。SquareShape である

2 つのクラスがたまたまいくつかのヘルパーを共有しているだけの場合、継承はやりすぎです。代わりに コンポジション(合成) を使いましょう。一方のクラスがもう一方のインスタンスを属性として保持し、その属性を通じてそれを使うのです。その形は「is-a」ではなく「has-a(〜を持つ)」です。

class Logger:
    def log(self, msg):
        print("[log]", msg)

# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
    def run(self):
        self.log("starting")

# composition: Worker HAS-A Logger
class WorkerComposes:
    def __init__(self):
        self.logger = Logger()

    def run(self):
        self.logger.log("starting")

どちらも機能します。合成版のほうが通常は優れています。その理由は次のとおりです。

  • インターフェースを小さく保つ。 WorkerComposesrunlogger だけを公開します。WorkerInheritslog も公開します -- 呼び出し側は、それが意図されていたかどうかにかかわらず、worker.log(...) と直接書くことができます。

  • ライフタイムを分離する。 ロガーは差し替えたり、ワーカー間で共有したり、ワーカーごとに異なる方法で構築したりできます。これらはすべて Worker クラスに触れることなく行えます。サブクラスはその基底にボルトで固定されています。

  • メソッド名の衝突を避ける。 どちらも run を定義する 2 つの基底は、あるクラスが両方から継承しようとすると衝突します。2 つの属性は決して衝突しません。

実践的な目安は次のとおりです。

  • サブクラスが本当に基底の一種 であり、基底のすべてのメソッドがそれにとっても意味をなす場合に継承を使ってください -- SquareShape であり、MemoryErrorException です。

  • それ以外のすべてにはコンポジションを使ってください -- 別のクラスの 振る舞い だけが欲しくて、そのアイデンティティは欲しくない場合です。ほとんどの実際のコードは、継承よりもはるかに多くコンポジションを使っています。