本稿では、インターフェース設計によって、クラスの種類を判別する分岐を無くし、コードをすっきりさせる方法について解説します。
はじめに、似たようなクラスをいくつも作る際の問題点を挙げます。そして、それを解決するために、インターフェース、抽象クラスが必要であることと、実装上の手続きを解説します。
1. 似たクラスを沢山作る際の問題
典型的な例として、三角形クラス、四角形クラスがあり、それぞれはメンバー変数として底辺と高さを、メソッドとして面積を求める関数を持っているとします。
class Triangle():
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height / 2
class Rectangle():
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
これらのクラスのオブジェクトを生成し、格納されたリストを作ります。そして、リストから各要素を取り出し、メソッドareaを呼び出せるオブジェクトなのかを、isinstance関数を使って調べてから、areaメソッドを呼び出します。この時、クラスが2種類あるので、isinstance関数による型チェックは2つの分岐が必要です。
if __name__ == '__main__':
triangle = Triangle(5, 10)
rectangle = Rectangle(5, 10)
shape_list = [triangle, rectangle]
for shape in shape_list:
if isinstance(shape, Triangle):
print(shape.area())
if isinstance(shape, Rectangle):
print(shape.area())
今は、クラスが2つしか存在しないので、上記のコードで不都合はありません。
しかし、ひし形クラス等、他の図形クラスが追加され、3つ、4つ…とクラスの種類が増えたらどうでしょうか?クラスの種類が増える程、以下のような問題が顕在化してきます。
- 各クラスのコーディングに間違いが生じやすい。
- if文をいくつも書く必要があり、コードが非常に煩雑になってしまう。
ここで、まず継承を使うことが考えられます。つまり、基底クラスとなる図形クラスShapeを用意して、三角形クラス、四角形クラスなどを派生させるという方法です。
しかし、普通の継承では、以下のような問題が生じます。
- 派生クラスごとに、areaメソッドの内容が異なる。そのため、基底クラスでメソッドの内容を定義することができない。
- 新しく作った図形クラスにareaメソッドを追加(オーバーライド)し忘れる可能性がある。
そこで、抽象クラスという、特別な基底クラスを導入します。
2. 抽象クラス・抽象メソッドとは
上手く継承を行うには、基底クラスを「抽象クラス」とする必要があります。
ここで、「抽象メソッド」について述べておきます。
- 基底クラスのメソッドの中身は空にしておく。
- 空にしたメソッドが、「内容を持たないメソッドである」ことを明示させる。
- 基底クラスのメソッドに対して、派生クラスにオーバーライドを強制させる。したがって、オーバーライドし忘れても、エラーを投げてくれる。
これらを実現できるのが抽象メソッドです。
抽象クラス・抽象メソッドの実装方法
抽象メソッドを持ったクラスのことを、「抽象クラス」と呼びます。具体的には、Pythonのabcモジュールを使用して抽象クラスを定義することができます。
from abc import abstractclassmethod, ABC
class AbstractClass(ABC):
@abstractclassmethod
def method(self):
pass
ちなみに、抽象クラスは派生しないと実体(インスタンス)を作ることができません。
文字通り、「抽象的」なクラスなのです。
試しに抽象クラスのインスタンスを作ろうとすると、次のようにエラーとなります。
object = AbstractClass()
>> TypeError: Can't instantiate abstract class AbstractClass with abstract methods area
3. 抽象クラスによってインターフェースを実現する
実は、Pythonにはインターフェース(interface)という明示的な概念はありません。Javaなどの言語には存在します。ちなみに、Javaにおけるインターフェースとは、特殊なクラスの種類のようなものなのですが、「スッキリわかるJava入門」では次のように述べられています。
- 全てのメソッドは抽象メソッドである。
- 基本的にフィールド(メンバー変数)を1つも持たない。
- フィールドとして、定数だけは許される。
※「定数」は、Pythonで言う所の「クラス変数」に相当します。
これはまさに、上で述べた抽象クラスとほぼ同じです。
インターフェースの実装例
では、インターフェースを実装してみて、何が起こるかを見ていきましょう。ここでは、先ほどの三角形、四角形クラスに加えて、ひし形クラスも追加しました。
- インターフェースとしての抽象クラスShapeを実装する。
- 各派生クラスでは、抽象メソッドareaの具体的な内容を実装する。
クラスの設計は、下のクラス図のようになります。
from abc import abstractclassmethod, ABC
# 抽象クラス
class Shape(ABC):
@abstractclassmethod
def area(self):
pass
class Triangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height / 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Diamond(Shape):
def __init__(self, vertical_diagonal, horizontal_diagonal):
self.vertical_diagonal = vertical_diagonal
self.horizontal_diagonal = horizontal_diagonal
def area(self):
return self.vertical_diagonal * self.horizontal_diagonal / 2
if __name__ == '__main__':
triangle = Triangle(5, 10)
rectangle = Rectangle(5,10)
diamond = Diamond(2, 10)
shape_list = [triangle, rectangle, diamond]
for shape in shape_list:
if isinstance(shape, Shape):
print(shape.area())
インターフェースによって最も嬉しいことは、クラスのメソッドを呼び出す際に、呼び出す側は、「そのクラスがShapeクラス(の派生クラス)であることだけ知っていればよく、具体的な派生クラスのどれに当たるかは気にしなくていい」ということです。
このように、抽象クラスやインターフェースを用意し、複数の派生クラスで同じ名前のメソッドを定義する。そして、派生クラスの種類を意識せずにメソッドを呼び出す。オブジェクト指向におけるこのような仕組みを「ポリモーフィズム」と言います。
インターフェースを導入する前は、クラスの数だけ分岐が必要でした。しかし、isinstanceによって、Shapeオブジェクトかどうかを判定するだけでよいのです。
注意点:
Pythonは動的型付けの言語です。C++やJavaなど、静的型付けの言語では、コンテナに格納できるオブジェクトの型があらかじめ決まっていますが、Pythonでは、リストに複数の型(複数の種類のクラス)のオブジェクトを格納することができます。
まとめ
本稿では、インターフェースが有効な場面と、Pythonにおける実装方法について解説してきました。ここで挙げた例は、面積を求めるだけの非常にシンプルなクラスでしたが、より多くのメソッドを持つ派生クラスが、更に何個も存在する場合に、インターフェースは威力を発揮します。
参考:
「スッキリわかるJava入門 第3版」中山清喬 (著), 国本大悟 (著), 株式会社フレアリンク (監修)
「独習Python」山田 祥寛 (著)
コメント