前回まででオブジェクト指向の前半戦は終了です。後半戦は前半の知識を前提としているので、もし難しいなと思ったら前半の復習をされることをオススメします。
後半の流れ
後半は「継承」という概念が主なテーマとなります。継承は具体的にはクラスAがすでにあり、そのクラスAをベースにして機能拡張をしたクラスBを作るという手法になります。
手法について扱うだけであればさほど苦労はしないと思いますが、継承のメリットと正しい使い方を理解することは難しいかもしれません。そのため、前半戦と同じように「なぜそうするか」や「どうあるべきか」といった話に比重を置きます。かなり冗長になってしまうかもしれませんが、ご容赦願います。継承についてある程度学んだら、簡単にGUIの使い方を扱い、GUIを使って継承を実際に利用してみたいと思います。
概要
最初に難しい話を繰り返しても眠くなってしまうでしょうし、とりあえずざっくりと継承のやりかたについてお話します。
まず以下の図を見てください。
左側にあるのがクラスAで右側にあるのがクラスBとなります。クラスBはクラスAを継承して作られており、内部にクラスAの要素を引き継いでいることがわかります。「クラスBがクラスAを持っている」のではなく、「クラスBはクラスAをベースに作られている」ことに注意してください。
さて、これを実際にコードに書いてみましょうか。まずはクラスAです。
class ClassA:
def __init__(self):
self.var_a = 'class a'
def print_a(self):
print('this method is defined in ' + self.var_a)
a = ClassA()
a.print_a()
# this method is defined in class a
var_aというインスタンス変数とprint_aというメソッドを持っています。ここまでは今までに学んだことであり、特に新しいものではありません。
次にこれを継承するクラスBを実装します。
class ClassB(ClassA): # <--- 継承の宣言
def __init__(self):
self.var_b = 'class b'
ClassA.__init__(self) # <--- 親クラスの初期化
def print_b(self):
print('this method is defined in ' + self.var_b)
print('parent of ' + self.var_b + ' is ' + self.var_a)
注目して欲しい点はいくつかありますが、まず一番最初に知る必要があるのは文法です。一行目のクラスBの宣言の後ろに () で ClassA が記載されていることがわかります。これが継承の宣言です。これをすることで「クラスBはクラスAを継承する」ことができるようになります。そして次は、4行目の "ClassA.__init__(self)" です。これはクラスBの親クラスであるクラスAの初期化です。
思い出してください。コンストラクタである__init__メソッドは引数をとると、その引数を初期化に利用するのでした。今回は引数がselfだけなので、初期化時に特に値を渡さなくてよいのですが、引数が2つ以上ある場合は値を渡さないとエラーになります。クラスAを継承したクラスBのなかで、「クラスAをどのように初期化するか」を指定する必要があり、この4行目はそれを実行しています。今回は親クラスに引数がないのですが、親クラスに引数がある場合は、子クラスでの初期化時に親クラスに引数を渡す必要があります。
最後にprint_bを見てもらいたいですが、なかでself.var_aを参照していることがわかります。Bの中でAのインスタンス変数を使っているということです。
とりあえず、このクラスBを使ってみましょうか。
b = ClassB()
b.print_b()
# this method is defined in class b
# parent of class b is class a
b.print_a()
# this method is defined in class a
まず、クラスBのインスタンスは当然ながらクラスBのメソッドを呼び出すことができます。出力を見てもらうとわかるように、クラスAのインスタンス変数を利用することができていますね。
親のインスタンス変数だけでなく、親のメソッドも利用することが可能です。クラスBのインスタンスからクラスAで定義されている print_a メソッドを呼び出していますが、特にエラーなく動作していることがわかります。
だいたいどういう使い方をするかわかっていただけたでしょうか。
継承を使うメリット
先ほどの例から、継承の使い方はわかっていただけたかと思います。ただ、問題となるのはなぜそのようなことをするのかだと思います。
たとえば先程の例であれば、わざわざクラスAを作って、クラスBに継承させるなどという面倒なことをせず、いきなり定義してしまえばよいです。たとえば以下のようなコードを書くと、先ほどのクラスAなしで、クラスBに先ほどとまったく同じことをさせることができます。
class ClassB:
def __init__(self):
self.var_a = 'class a'
self.var_b = 'class b'
def print_a(self):
print('this method is defined in ' + self.var_a)
def print_b(self):
print('this method is defined in ' + self.var_b)
print('parent of ' + self.var_b + ' is ' + self.var_a)
どうみてもこっちのほうがシンプルですよね。実際、今回のようなクラスA、Bの使い方はあくまでもサンプルであり、これが実コードであれば設計としてはナンセンスです。では、具体的にどのような場合に継承を使うメリットがあるのでしょうか。それは主に以下の場合となります。
- すでに利用している自分が作った既存のクラスを継承する場合
- 標準ライブラリやほかの人が作ったクラスを継承する場合
- 親に多数の子供がいる場合
- GUIを利用する場合
上記のいくつかについては細かい話が必要なため、この回ですべてをカバーすることはできません。ただ、なんとなくでかまわないので全体像をつかんでもらいたいと考えているため、軽くそれぞれについて解説したいと思います。
なお、これらの継承の使いかたは、ほかのメジャーなオブジェクト指向言語であるJavaやC++も意識したうえで記載しています。正直なところPythonの継承はこれらの言語より単純であり、実現できることもそれほど多くありません。「こんなことPythonではやらないよ」といった話も出てくるとは思いますが、ご了承ください。
自分が作った既存クラスを継承
これは自分たちが作った既存のクラスがすでに存在していて、それに変更は加えることはできないものの、似たクラスが必要な場合に利用します。
たとえば、以下の図を見てください。
ここではクラスAはクラスCから利用されています。このクラスAに機能を追加し、それを別の用途で利用したいとします。今回はクラスDからの利用としましょう。その際、クラスA自体のコードを変更してしまうのが手っ取り早いのですが、常にそれができるとは限りません。その際、クラスAはキープしたままにし、継承を利用して変更を加えるという利用方法があります。
個人的にこの使い方はあまり好きではないです。なぜなら継承ではなく、コンポジション(クラスBがクラスAを持つ)で実現できてしまう場合が多いからです。継承とコンポジションの使い分けの話をするにはまだ早いので、これは後ほど改めて扱います。
標準ライブラリやほかの人が作ったクラスを継承
この例は先程の例と似ています。ただ、変更できない理由が「標準ライブラリ」であることや「他人が作ったコード」であることに起因しています。
これはポリモーフィズムという機能に起因してJavaやC++では多用されるものの、Pythonでは「型の宣言」がないのでそれほど利用されない気がします。こちらもコンポジションで実現可能です。ポリモーフィズムについては4つめの利用法で紹介します。
ただ、「元のクラスに備えられているメソッドを呼び出せる」という点が有用な場合は、コンポジションでいちいちラッパーメソッドを定義する必要がないので便利です。
難しい話はさておき、簡単なサンプルを書いてみます。今回は標準ライブラリのリストを拡張し、整数のみを格納できるinteger_listを作成してみます。
class IntList(list):
def __init__(self):
self.int_type = type(0)
list.__init__(self)
def append(self, elem):
if(self.int_type == type(elem)):
list.append(self, elem)
else:
print('error: "' + str(elem) + '" is not Int')
il = IntList()
il.append(3)
il.append(5)
il.append('hello')
# error: "hello" is not Int
il.append(7)
print(il[1])
# 5
print(il)
# [3, 5, 7]
print(type(il))
# <class '__main__.IntList'>
listを継承してIntListを作成していますね。コンストラクタについては特に悩む必要はないと思いますが、appendメソッドについては「オーバーライド」と呼ばれる継承のテクニックを使っています。オーバーライドは処理を上書きするという役割があるのですが、細かいことは後の回にて扱います。
利用方法はlistとほとんど同じであることがわかりますね。ただ、appendメソッドを読んだ場合の処理はオリジナルのlistではなく、IntListでオーバーライドされたものが呼び出されています。
なお今回、異なる型は格納しないという実装にしましたが、本来は例外を投げるようにすべきかもしれません。ただ、例外についてはまだ学んでいないのでこのような実装としました。
親に多数の子供がいる場合
以下の図を見てください。
親クラスGraphicから子クラスRectangle、Triangle、Circleを作っています。このとき、Rectangle、Triangle、Circleは利用者であるDrawerからあたかも親クラスのGraphicであるかのように使えるというのがポリモーフィズムの特徴です。
Pythonだと、型と変数が結びついていないのでメリットがわかりにくいのですが、Javaなどだと以下のように親クラスに子クラスを代入することが可能です。
Graphic g1 = new Rectangle();
Graphic g2 = new Triangle();
Graphic g3 = new Circle();
これができると何がいいかというと、たとえば以下のようなコードが書けます。
String style = "rectangle";
Graphic graphic = null;
if(style.equals("rectangle")){
graphic = new Rectangle();
}else if(style.equals("triangle")){
graphic = new Triangle();
}
graphic.draw();
上記では与えられたstyleの文字列に従って、利用するクラスをRectangle、Triangleで切り替えられます。今回は初期化時にrectangleという文字を入力してしまっていますが、これをたとえばユーザーが何を入力したか読み取ったり、クリックされたボタンが何であったかなどといった動的な情報で「どの子クラスを利用するか」を切り替えることになるかもしれませんね。もちろん静的に子クラスを指定する場合も多々あります。
そして、先ほどのオーバーライドの例にあったようにgraphic.draw()を呼び出したタイミングで子クラスによって具体的な処理を変更することが可能です。また、親クラスとして扱っているのでRectangle、Triangleを使うコードを共有できるという強みもあります。
まぁ、これも話すと長くなりそうなので次回以降に回したいと考えています。
GUIの利用時の継承
Pythonに限らずさまざまなプログラミング言語において、GUIを使うためには継承が利用されることが非常に多いです。これは継承とGUIの仕組みの親和性が非常に高いためだと思われます。
以下の図を見てください。
これは過去にコラムで紹介した、私が作ったログ解析用のアプリケーションですが、アプリケーションを構成する要素であるWidgetが階層構造になっているのがわかりますね。つまりAはB、Cを持つ。BはDを持つという構成を作ることが可能です。
また、ここにあるWidget 1、2、3はGUIのパーツであり、マウスやキーボードの動きへの対応や、画面の出力などの共通点は多いです。これらの共通点は親クラスでまとめらており、具体的なウィジェットの特徴などは子クラスで定義されています。
先ほどの「親に多数の子供がいる場合」の継承の使い方をより複雑にしたのが一般的なGUIのライブラリだと思っていただければ大丈夫だと思います。GUIについては次回以降詳細に扱いますので、今回の解説はこのあたりにしておきたいと思います。
演習1
以下のクラスを継承し、オリジナルのクラスのインスタンス変数aをプリントするメソッドを実装してください。
class ClassA:
def __init__(self):
self.a = 'hello'
演習2
以下のクラスを継承し、オリジナルのクラスのインスタンス変数a, bを足した値をプリントするメソッドを実装してください。なお、継承したクラスのコンストラクタも引数を2つ受け取り、それを親クラスの初期化に利用してください。
class ClassA:
def __init__(self, a, b):
self.a = a
self.b = b
演習3
listを継承し、コンストラクタに与えた引数のタイプだけ格納できるリストクラスを作成してください。たとえば
ml = MyList('a')
とすると、文字列型しか受け取らなくなります。
次回も今回に引き続き、継承について解説していきます。
執筆者紹介伊藤裕一(ITO Yuichi)シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。 もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。 Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択 詳細(英語)はこちら |
---|