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