はじめに

最近Python3のclassについて復習したので、ちょっと復習のノートを取りたいと思います。
今日は多重継承の中の菱形継承問題について話します。あくまでも個人の見解なので、間違いがあったらご容赦ください。

継承とは?

まず簡単にPython3の継承について復習してみましょう。一つのクラスはもう一つのクラスを継承することによって、継承されたクラスの属性とメソードを利用することができます。次の例を見てみましょう。

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

    def sayName(self):
        print("My name is {}.".format(self.name))


class Student(Person):
    def __init__(self, name, id):
        super().__init__(name)
        self.id = id

    def sayId(self):
        print("My id is {}".format(self.id))

ここで、PersonとStudentの二つのクラスを定義します。StudentはPersonを継承することで、Personの属性とメソードが使えるようになります。継承の仕方は簡単で、Studentクラスを定義する時、Student(Person)のように、継承したいクラスの名前を入れて、super().__init__()を使えば良い。
ここで、本来super(Student, self)のようにsuper(クラス名, self)を書く必要があるが、Python3では省略しても大丈夫です。(個人的には書くのが好きです。ここで説明するため省略しました。)

では、予想通りに動くかどうか実行してみましょう。

Tom = Student("Tom", 123)
Tom.sayName()
Tom.sayId()

結果は以下のようになりました。

My name is Tom.
My id is 123

PersonクラスのnameとsayName()を成功に継承しました。

実は、昔(Python2の時)はsuper()を使わずに継承する方法もありました。superの部分を次のように明示的に書けば良いです。

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

    def sayName(self):
        print("My name is {}.".format(self.name))


class Student(Person):
    def __init__(self, name, id):
        Person.__init__(self, name)
        self.id = id

    def sayId(self):
        print("My id is {}".format(self.id))

もう一回実行してみましょう。

Tom = Student("Tom", 123)
Tom.sayName()
Tom.sayId()

結果は以下のようになりました。

My name is Tom.
My id is 123

同じく継承できました。

菱形継承とは?

次のようなシチュエーションを考えます。以下のコードのように、Aはsuper classで、B, CはそれぞれAを継承するclassです。DはBとCを継承するクラスとする。

class A():
   pass


class B(A):
   pass


class C(A):
   pass


class D(B, C):
   pass

菱形継承とは、上記のようなABCDの間の継承関係のことを指しています。
では、passを書き換えて、少しコードを補完しましょう。次のように、初期化する時__init__に入ると出る状況をprintしてみましょう。

class A():
   def __init__(self):
       print("enter A")
       print("leave A")


class B(A):
   def __init__(self):
       print("enter B")
       super(B, self).__init__()
       print("leave B")


class C(A):
   def __init__(self):
       print("enter C")
       super(C, self).__init__()
       print("leave C")


class D(B, C):
    def __init__(self):
        print("enter D")
        super(D, self).__init__()
        print("leave D")


d = D()

結果が次のようになっています。

enter D
enter B
enter C
enter A
leave A
leave C
leave B
leave D

では、super()を使わないバージョンはどうでしょう?

class A():
   def __init__(self):
       print("enter A")
       print("leave A")


class B(A):
   def __init__(self):
       print("enter B")
       A.__init__(self)
       print("leave B")


class C(A):
   def __init__(self):
       print("enter C")
       A.__init__(self)
       print("leave C")


class D(B, C):
    def __init__(self):
        print("enter D")
        B.__init__(self)
        C.__init__(self)
        print("leave D")


d = D()
enter D
enter B
enter A
leave A
leave B
enter C
enter A
leave A
leave C
leave D

区別が分かるでしょうか?ここでは、Aに2回enterとleaveをしました。その原因は、深さ優先探索で実行しているらしいです。実際、このような書き方は深さ優先探索で色々不便を招くため、super()を使うと提唱されているらしいです。

しかし、super()も問題がありました。
次の例を見てみましょう。Personが一番上のクラスで、StudentとWorkerはPersonを継承します。最後に、WorkingStudentはStudentとWorkerを継承します。

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Student(Person):
    def __init__(self, name, age, id):
        super(Student, self).__init__(name, age)
        self.id = id


class Worker(Person):
    def __init__(self, name, age, skilllevel):
        super(Worker, self).__init__(name, age)
        self.skilllevel = skilllevel


class WorkingStudent(Student, Worker):
    def __init__(self, name, age, id, skilllevel):
        super(WorkingStudent, self).__init__(name, age, id, skilllevel)

    def __str__(self):
        return "Name : {}\nAge : {}\nID : {}\nSkillLevel : {}".format(self.name, self.age, self.id, self.skilllevel)

tom = WorkingStudent("tom", 20, 100, 999)

ここで、各クラスにはそれぞれの__init__関数があるため、予想通りに動けません。

結果:

Traceback (most recent call last):
File "classtest4.py", line 76, in <module>
tom = WorkingStudent("tom", 20, 100, 999)
File "classtest4.py", line 71, in __init__
super(WorkingStudent, self).__init__(name, age, id, skilllevel)
TypeError: __init__() takes 4 positional arguments but 5 were given

では、どうすれば良いでしょう?色々調べて、やってみた結果、解決案を発見しました。次のようにStudentとWorkerの引数の中に、WorkingStudentの引数を定義する必要があります。さらに、WorkingStudentの一番目のsuper classであるStudentの中にsuperの部分も修正する必要があります。

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Student(Person):
    def __init__(self, name, age, id, skilllevel):
        super(Student, self).__init__(name, age, id, skilllevel)
        self.id = id


class Worker(Person):
    def __init__(self, name, age, id, skilllevel):
        super(Worker, self).__init__(name, age)
        self.skilllevel = skilllevel


class WorkingStudent(Student, Worker):
    def __init__(self, name, age, id, skilllevel):
        super(WorkingStudent, self).__init__(name, age, id, skilllevel)

    def __str__(self):
        return "Name : {}\nAge : {}\nID : {}\nSkillLevel : {}".format(self.name, self.age, self.id, self.skilllevel)


tom = WorkingStudent("tom", 20, 100, 999)
print(tom)

結果:

Name : tom
Age : 20
ID : 100
SkillLevel : 999

理由はまだ十分理解できていないので、分かる方がいったら教えてください。
このようなやり方はプログラムの設計原理に反しています。
あるクラスと他のクラスが一緒に継承される時、他のクラスの引数を知らないといけないなんて、直感的におかしいと分かるでしょう。
したがって、このようなことを避けるため、多重継承する時、__init__を一個に限定するというミックスイン(Mix-in)原則が提出されました。つまり、継承されるクラス群の中に、最大一個だけ__init__を持つことが許されます。

他には、次のようにsuper()を使わずに明示的に継承する方法があります。ただし、ここでは上記で説明したように、菱形の一番上のクラスに二回enterする問題が発生するので、理想的な解決案ではありません。
次のコードと結果を見てみましょう。

class Person(object):
    def __init__(self, name, age):
        print("enter Person")
        self.name = name
        self.age = age
        print("leave Person")

class Student(Person):
    def __init__(self,  name, age, id):
        print("enter Student")
        Person.__init__(self, name, age)
        self.id = id
        print("leave Student")


class Worker(Person):
    def __init__(self, name, age, skilllevel):
        print("enter Worker")
        Person.__init__(self, name, age)
        self.skilllevel = skilllevel
        print("leave Worker")

class WorkingStudent(Student, Worker):
    def __init__(self, name, age, id, skilllevel):
        print("enter WokingStudent")
        Student.__init__(self, name, age, id)
        Worker.__init__(self, name, age, skilllevel)
        print("leave WorkingStudent")

    def __str__(self):
        return "Name : {}\nAge : {}\nID : {}\nSkillLevel : {}".format(self.name, self.age, self.id, self.skilllevel)

tom = WorkingStudent("tom", 20, 100, 999)
print()
print(tom)

結果:

enter WokingStudent
enter Student
enter Person
leave Person
leave Student
enter Worker
enter Person
leave Person
leave Worker
leave WorkingStudent

Name : tom
Age : 20
ID : 100
SkillLevel : 999

終わりに

結局菱形継承問題で分かったことは、複数の__init__の状況を避けるしかないということです。他にもし解決策があったら、教えていただけるとありがたいです。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.