LoginSignup
15

More than 5 years have passed since last update.

Python3の多重継承(菱形継承)について

Last updated at Posted at 2018-05-04

はじめに

最近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__の状況を避けるしかないということです。他にもし解決策があったら、教えていただけるとありがたいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15