[解決!Python]クラスを継承するには解決!Python

Pythonで単一継承する方法、メソッドをオーバーライドする方法、多重継承する方法、多重継承とMRO、協調的な多重継承について説明する。

» 2023年12月01日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「解決!Python」のインデックス

連載目次

* 本稿は2022年2月1日に公開された記事をPython 3.12.0で動作確認したものです(確認日:2023年12月1日)。


単一継承

class B:
    def __init__(self, a):
        print('B init')
        self.a = a

    def some_method(self):
        print(f'a: {self.a}')

class D(B):
    def __init__(self, a, b):
        print('D init')
        super().__init__(a)
        self.b = b

    def another_method(self):
        self.some_method()
        print(f'b: {self.b}')


d = D(1, 2)
# 出力結果
#D init
#B init
d.some_method()  # a: 1
d.another_method()
# 出力結果
a: 1
b: 2


 説明は以下の「単一継承」を参照のこと。

メソッドのオーバーライド

class B:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def some_method(self):
        print(f'a: {self.a}, b: {self.b}')

    def calc_sum(self):
        return self.a + self.b

class D(B):
    def __init__(self, a, b, c, d):
        super().__init__(a, b)
        self.c = c
        self.d = d

    def another_method(self):
        self.some_method()
        print(f'c: {self.c}, d: {self.d}')

    def calc_sum(self):  # overrides B's calc_method
        tmp = super().calc_sum()
        # tmp = B.calc_sum(self)
        return tmp + self.c + self.d


d = D(1, 2, 3, 4)
print(d.calc_sum())  # 10


 説明は以下の「メソッドのオーバーライド」を参照のこと。

多重継承

class B1:
    def __init__(self, a):
        print('B1 init')
        self.a = a

class B2:
    def __init__(self, b):
        print('B2 init')
        self.b = b

class D(B1, B2):
    def __init__(self, a, b, c):
        print('D init')
        B1.__init__(self, a)
        B2.__init__(self, b)
        self.c = c

d = D(0, 1, 2)
print(f'a: {d.a}, b: {d.b}, c: {d.c}'# a: 0, b: 1, c: 2


 説明は以下の「多重継承」「多重継承とMRO」を参照のこと。

協調的な多重継承

class B0:
    def __init__(self):
        print('B0 init')

class B1(B0):
    def __init__(self, a, **kwargs):
        print('B1 init')
        super().__init__(**kwargs)
        self.a = a

class B2(B0):
    def __init__(self, b, **kwargs):
        print('B2 init')
        super().__init__(**kwargs)
        self.b = b

class D(B1, B2):
    def __init__(self, c, **kwargs):
        print('D init')
        super().__init__(**kwargs)
        self.c = c

d = D(a=0, b=1, c=2)
print(f'a: {d.a}, b: {d.b}, c: {d.c}')


 説明は以下の「協調的な多重継承」を参照のこと。

単一継承

 Pythonでクラスを継承する際には、単一継承もしくは多重継承を行える。単一継承では、「class 派生クラス名(規定クラス名):」のようにclass文の先頭でクラス名に続けて継承元のクラスを指定する。

 例えば、以下のようなクラスがあり、そのクラスを継承するクラスを定義したいとする。

class B:
    def __init__(self, a):
        print('B init')
        self.a = a

    def some_method(self):
        print(f'a: {self.a}')


 このクラスBはデータ属性aを持ち、__init__メソッドではこの属性の初期化を行っている(Bは継承元を意味する「Base」を省略したもの)。some_methodメソッドは属性aの値を表示するだけだ。なお、このクラス定義では「class B:」とだけ書いているが、これはPythonに組み込みのobjectクラスを継承することを意味している。「class B(object):」と書いても同様である。

 このクラスを継承するクラスDの定義例を以下に示す(Dは派生を意味する「Derived」を省略したもの)。

class D(B):
    def __init__(self, a, b):
        print('D init')
        super().__init__(a)
        self.b = b

    def another_method(self):
        self.some_method()
        print(f'b: {self.b}')


 クラスDはクラスBを継承するので、class文は「class D(B):」で始まっている。また、__init__メソッドではsuper関数を使ってクラスBを参照するオブジェクトを取得し、それを介してクラスBの__init__メソッドを呼び出している。クラスDの__init__メソッドは2つのパラメーターに値を受け取り、そのうちの1つをクラスBの__init__メソッドに渡すことで、継承元であるクラスBのインスタンスが持つ属性aを初期化している。残りの1つで自身が持つ属性bの初期化も行っている。

 another_methodメソッドはクラスBで定義されているsome_methodメソッドを呼び出して、その属性aの値を表示した後に、派生クラスDのインスタンスが持つ属性bの値を表示している。

 実際にクラスDのインスタンスを生成して、上記2つのメソッドを呼び出すと次のようになる。

d = D(1, 2)
# 出力結果
#D init
#B init
d.some_method()  # a: 1
d.another_method()
# 出力結果
a: 1
b: 2


 「super().__init__(1)」呼び出しにより、クラスBの__init__メソッドが呼び出され、「B init」と出力されていることに注意。

メソッドのオーバーライド

 派生クラスでは基底クラスで定義されているメソッドをオーバーライドできる。オーバーライドするには、基底クラスで定義されているメソッドと同じ名前のメソッドを派生クラスで定義すればよい。

 例えば、次のようなクラスがあり、これを基に派生クラスDを定義したいものとする。

class B:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def some_method(self):
        print(f'a: {self.a}, b: {self.b}')

    def calc_sum(self):
        return self.a + self.b


 先ほどとほぼ同様だが、今度は基底クラスに2つの属性がある。some_methodメソッドも先ほどと同様に属性の値を出力するだけだ。calc_sumメソッドは2つの属性の和を計算する。

 この派生クラスの定義例を以下に示す。

class D(B):
    def __init__(self, a, b, c, d):
        super().__init__(a, b)
        self.c = c
        self.d = d

    def another_method(self):
        self.some_method()
        print(f'c: {self.c}, d: {self.d}')

    def calc_sum(self):  # overrides B's calc_method
        tmp = super().calc_sum()
        #tmp = B.calc_sum(self)
        return tmp + self.c + self.d


 こちらも先ほどと同様だ。大きな違いはcalc_sumメソッドをこのクラスでも定義している(=calc_sumメソッドをオーバーライドしている)ことだ。このメソッドの中では「super().__calc__sum()」として、クラスBで定義されている同名のメソッドを呼び出している点にも注意しよう。オーバーライドした側から、元のメソッドはこのようにして呼び出せる。あるいは「B.calc_sum(self)」と書いてもよい。

 このクラスのインスタンスを生成して、利用する例を以下に示す。

d = D(1, 2, 3, 4)
print(d.calc_sum())  # 10


多重継承

 多重継承を行うには、class文で基底クラスをカンマ区切りで並べる。例えば、以下のような2つのクラスがあり、それらを継承したクラスを定義したいとする。

class B1:
    def __init__(self, a):
        print('B1 init')
        self.a = a

class B2:
    def __init__(self, b):
        print('B2 init')
        self.b = b


 これらのクラスは基底クラスを特に指定していないので、objectクラスを継承するものだ。objectクラスの__init__メソッドは実質的には何もしないので、__init__メソッドでは「super().__init__()」のようにしてobjectクラスの__init__メソッドを呼び出してはいない。

 クラスB1とクラスB2を基底クラスとするクラスDの定義例を以下に示す。

class D(B1, B2):
    def __init__(self, a, b, c):
        print('D init')
        B1.__init__(self, a)
        B2.__init__(self, b)
        self.c = c


 class文は「class D(B1, B2):」のようにして始めることで、クラスDがクラスB1とB2を継承していることが分かる。__init__メソッドでは両者のインスタンスが持つ属性を初期化するために「B1.__init__(self, a)」「B2.__init__(self, a)」としてそれぞれのクラスの__init__メソッドを呼び出している。

 このクラスのインスタンスを生成して、使用する例を以下に示す。

d = D(0, 1, 2)
print(f'a: {d.a}, b: {d.b}, c: {d.c}'# a: 0, b: 1, c: 2


多重継承とMRO

 ところで、「B1.__init__(self, a)」のようにしてではなく、「super().__init__(a)」のようにして__init__メソッド呼び出しを行うにはどうしたらよいだろう。例えば、以下のコードを考えてみる(ここではB1とB2がobjectクラスを継承するのではなく、B0を継承するようにしてあるが、内容的にはあまり変わらない。単にB1とB2の基底クラスがパラメーターのない__init__メソッドを持っていることを明記しただけだ)。

class B0:
    def __init__(self):
        print('B0 init')

class B1(B0):
    def __init__(self, a):
        print('B1 init')
        super().__init__()
        self.a = a

class B2(B0):
    def __init__(self, b):
        print('B2 init')
        super().__init__()
        self.b = b

class D(B1, B2):
    def __init__(self, a, b, c):
        print('D init')
        B1.__init__(self, a)
        B2.__init__(self, b)
        self.c = c


 クラスB0の__init__メソッドでは何らかの属性の初期化などは行っていない。そのため、パラメーターも持たない。よって、B0を継承しているB1とB2の__init__メソッドでは「super().__init__()」として基底クラスであるB0の__init__メソッドを呼び出すようにしている。

 クラスDの__init__メソッドでは、先ほどの例と同じようにB1とB2の__init__メソッドを呼び出すようにした。これは、B1の属性aとB2の属性bを適切に初期化する方法が思いつかなかったからだ。ここを「super().__init__(……)」のようにして呼び出せば、連鎖的にB0、B1、B2の__init__メソッドが(全て一度だけ)呼び出されるようにすることが目的といえる。

 取りあえず、インスタンスを生成してみると、実は上のコードでは例外が発生する。

>>> d = D(0, 1, 2)
>>> D init
B1 init
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __init__
  File "<stdin>", line 4, in __init__
TypeError: B2.__init__() missing 1 required positional argument: 'b'



 上の出力を見ると、クラスDの__init__メソッドからクラスB1の__init__メソッドが呼び出されたところで例外が発生しているようだ。エラーメッセージからは「B2.__init__メソッドには位置引数bがあるのに、それが指定されていない」ことが判断できる。しかし、クラスB1の__init__メソッドでは基底クラスであるB0の__init__メソッドを呼び出すつもりで「super().__init__()」を実行している。これがなぜクラスB2の__init__メソッドを呼び出すことになっているのだろう。

 クラスを継承したときに、メソッド(__init__メソッドに限らない)がどのように呼び出されるかを決定するのが、MRO(Method Resolution Order、メソッド解決順序)だ。これはクラス階層の中で同名のメソッドが複数定義されている際に、どのメソッドを優先するかにも影響する。

 詳しい説明は省略するが、MROを調べるにはそれぞれのクラスでmroメソッドを呼び出すとよい。クラスDでmroメソッドを呼び出すと次のようになる。

D.mro()
# 出力結果
#[<class '__main__.D'>, <class '__main__.B1'>, <class '__main__.B2'>,
# <class '__main__.B0'>, <class 'object'>]


 これはD→B1→B2→B0→objectという順番でメソッドが検索され、クラスに該当するメソッドがあれば、それが呼び出されることを意味する。上のコードでは「B1.__init__(self, a)」呼び出しで例外が発生していたが、B1.__init__メソッドでは「super().__init__()」呼び出しを行っていた。このときに呼び出されるメソッドがどう検索されるかといえば、MROに従って「D→B1→B2→B0→object」でB1の次にあるB2がまず検索されることになる。そして、B2.__init__メソッドはパラメーターを1つ必要としていたので、エラーとなったということだ。

 多重継承をしている場合、このようにsuper()を経由したメソッド呼び出しが自分の考えとは異なる場合がある。同時に、対象のメソッドのシグネチャが全てのクラス内で同じでなければ上の例のように例外が発生する。この場合は、「super().__init__(……)」呼び出しは最終的にB0.__init__メソッドに到達するので、全ての呼び出しが無引数の呼び出し(と同様なもの)になっていなければならない。

 これをうまく解決するのが、いわゆる協調的な多重継承と呼ばれるテクニックだ。

協調的な多重継承

 協調的な多重継承では、__init__メソッドに可変長キーワード引数(**kwargs)を持たせることで全ての__init__メソッドが同一のシグネチャとして扱えるようにする。以下に例を示す。

class B0:
    def __init__(self):
        print('B0 init')

class B1(B0):
    def __init__(self, a, **kwargs):
        print('B1 init')
        super().__init__(**kwargs)
        self.a = a

class B2(B0):
    def __init__(self, b, **kwargs):
        print('B2 init')
        super().__init__(**kwargs)
        self.b = b

class D(B1, B2):
    def __init__(self, c, **kwargs):
        print('D init')
        super().__init__(**kwargs)
        self.c = c


 クラスB1、B2、Dの__init__メソッドのパラメーターリストには、それぞれのメソッドが必要とするパラメーターと可変長キーワード引数があることに注目しよう。そして、自分が処理した残りのパラメーターを全て「super().__init__(**kwargs)」として渡している。

 これにより、__init__メソッドがどのような呼び出しの連鎖につながるかを考えてみよう。MROは先ほどと同じく「D→B1→B2→B0→object」だ。まず、「D(a=1, b=2, c=3)」のようにしてクラスDのインスタンスを生成する。これにより、クラスDの__init__メソッドはパラメーターcに3が渡されて属性cの初期化に使われ、**kwargsに残り(a=1とb=2)が渡される。

 次にクラスDの__init__メソッド内部での「super().__init__(**kwargs)」呼び出しにより、MROに従ってクラスB1の__init__メソッドが呼び出される。B1.__init__メソッドのパラメーターはaと**kwargsなので、パラメーターaには1が渡されて属性aの初期化に使われ、**kwargsには残ったb=2が渡される。

 そして、クラスB1の__init__メソッドの内部で「super().__init__(**args)」呼び出しが行われる。すると、今度はMROに従ってB2.__init__メソッドが呼び出される。B2.__init__メソッドのパラメーターはbと**kwargsなのでパラメーターbに2が渡されて属性bの初期化に使われ、**kwargsには何も渡されない。

 最後に、クラスB2の__init__メソッドの内部で「super().__init__(**args)」呼び出しが行われる。既に**kwargsは空なので、これは「super().__init__()」と同じであり、MROで次の候補であるB0クラスの__init__メソッドの呼び出しが問題なく行える。

 このようにして、クラスDでの一度の「super().__init__(…)」呼び出しにより、クラス階層にある全ての__init__メソッドが(全て一度だけ)呼び出されて、適切に初期化が行われるようになった。

 全ての__init__メソッドに可変長位置引数を持たせて、最後の__init__メソッドでそれらを一括して処理するといった方法も考えられるが、ここではコードは紹介しない。

 実際にインスタンスを生成して、それを使う例を以下に示す。

d = D(a=0, b=1, c=2)
# 出力結果
#D init  # 全てのクラスの__init__メソッドが一度だけ呼び出されている
#B1 init
#B2 init
#B0 init
print(f'a: {d.a}, b: {d.b}, c: {d.c}'# a: 0, b: 1, c: 2


 このように、多重継承を行う際のインスタンスの初期化には、基底クラスの__init__メソッドを明示的に呼び出す方法と、協調的な多重継承を行う方法がある。前者はクラス名をコード中に明記するのでコードのメンテナンスが面倒くさくなるかもしれない(基底クラスを変更すると__init__メソッド呼び出しも変更する必要がある)。また、後者ではインスタンス生成時にキーワード引数を使う必要があるので、少しコードの記述量が増える(ただし、どの値をどのクラスの初期化に使うかを明記するという意味では必要なコストといえるだろう)。

「解決!Python」のインデックス

解決!Python

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。