プログラミングについて調べごとをしていると、よく「インターフェース」という言葉が出てきます。
しかし、どういう意味だろうと思って調べてみても全然わからない......
加えてサンプルコードもJavaやC#ばかりで、Pythonのものはなかなかありません。
そこで今回はインターフェースについて、Pythonを使って考えてみたいと思います。
更新履歴
2025/06/06 : 初版
2025/06/07 : 文章の修正
環境
- Windows11
- Python3.12
- PyCharm Community Edition 2024.3.5
1. インターフェースの定義
インターフェースの一般的な定義は次の通りです。
オブジェクト指向プログラミングにおいて、
複数の種類のオブジェクトを
多態性によって統一的に利用するため、
境界部分の汎用的な共通規格を定義したもの。
インタフェース (情報技術) / ソフトウェアインタフェース
いまいちピンとこないかもしれませんが、一番重要なのは
複数の種類のオブジェクトを統一的に利用する
の部分です。
つまり、インターフェースとは 「一般化」の概念を表現するための機能 と言い換えることができます。
2. インターフェースのモチベ
具体例で考えてみましょう。
ここに円(Circle)、三角形(Triangle)、長方形(Rectangle)クラスがあります。
class Circle:
pass
class Triangle:
pass
class Rectangle:
pass
これらの図形は全て面積を計算できます。
そこで、2つの図形の面積を比較して同じであればTrue
、違えばFalse
を返す関数is_same_area
を考えましょう。
def is_same_area(
shape_1: Circle | Triangle | Rectangle,
shape_2: Circle | Triangle | Rectangle,
) -> bool:
pass
しかし、このTypeHintの書き方には問題があります。
もし扱う図形が台形、平行四辺形、菱形、正方形......と増えていったら、際限なく長くなってしまいます。
そこで、これらの図形を一般化した「平面図形」を考えてみましょう。
これを使うことができたら、随分すっきりとした書き方ができそうです。
def is_same_area(
shape_1: 平面図形, # 円も三角形も長方形もOK!
shape_2: 平面図形,
) -> bool:
pass
台形など扱う図形が増えたとしても、平面図形であれば同じように扱うことができます。
「複数の種類のオブジェクトを統一的に利用する」。
これはまさに、インターフェースの定義で触れた内容ですね。
そこで、「平面図形」を改めて「平面図形インターフェース」として考えることにしましょう。
3. インターフェースの表現方法
前節でインターフェースを使うメリットを見ましたが、考慮すべき問題があります。
それは「インターフェースをどう表現するか?」です。
言い方を換えれば「便利なのはわかるけど、プログラムでどう書けばいいのさ?」ということです。
3.1. インターフェースとクラスを区別したい!
これまで見てきた通り、インターフェースを「一般化」というキーワードで説明しました。
つまり、インターフェースにはいくつかの「具体例」となるクラスが紐づけられていると言えます。
プログラムを書くとき、インターフェースとクラスで全く同じ文法だとすると、折角性質を分けたのにどっちを指しているかわかりません。
class PlanarShape
class Circle
class Triangle
class Rectangle
そこでプログラミング言語制作者は、インターフェースとクラスで文法そのものを変えよう、と考えました。
例えばインターフェースとクラスを区別する「マーク」を強制すれば、混同することはなさそうです。
更に、インターフェースは名前の先頭に"I" ("I"nterface)を付けることで、視覚的にも区別しやすくなりそうです。
interface IPlanarShape
class Circle
class Triangle
class Rectangle
それだけではなく、インターフェースとクラスに明確な違いを設けることにしました。
それは、「インターフェースにはメソッドの中身(処理)を書くことができない」という制約です。
逆に何を書けるのかというと、「メソッドの名前、引数の変数名と型、戻り値の型」です。
インターフェースでは「メソッドの名前、引数、戻り値」に関するものだけを決めておき、紐づけられた具体的なクラスで処理を書く、という流れになります。
インターフェースを徹底的に抽象的なものとして定義したいという想いが伝わってきますね。
そして、「インターフェース」という単語が使われる理由も想像がつきます。
和訳すれば「境界面」。メソッドを使う人にとって、引数とは中身(処理)との入力側の境界面であり、戻り値とは中身(処理)との出力側の境界面に相当します。
これら「入出力の境界面(引数と戻り値)に関する取り決め(変数名や型)」という意味で、インターフェースという単語が使われているのです。
実際に、最初に触れたインターフェースの定義にも表れていますね。
境界部分の汎用的な共通規格を定義したもの。
「境界部分」というのはまさに境界面のことですから、「中身(処理)じゃなくて、境界面(引数と戻り値)の取り決めだよ」と言っている訳です。
以上を図にまとめてみると、次のようになります。
平面図形インターフェースでarea
メソッドの引数と戻り値を定義し、円クラス、三角形クラス、長方形クラスではそれぞれの面積公式を使って面積を計算します。
このように、同じarea
メソッドを使ったとしても中身の処理が異なる、というのを「多態性」と言います。
3.2. 困りごとと抽象クラス
インターフェースのメソッド内に処理を書かないという制約により、処理の流れをシンプルにすることができました。
しかし、これによって困りごとができてしまいました。
もし平面図形の体積を計算するvolume
メソッドを定義するとどうなるでしょうか?
平面図形の体積はどんな図形でも0
なので、処理を書く場所はどこか一箇所で済ませておきたいところです。
しかし、平面図形を取りまとめている平面図形インターフェースには処理を書くことができません。
するとどうなるかというと、「0
を返す」という処理を全ての平面図形クラスで実装しなければなりません。
これは面倒なことになりそうですね。
volume
メソッドのように複数のクラスで共通した処理になる場合、同じ処理を何回も書くことになってしまいます。
でもインターフェースには処理を書きたくない......
そんな悩みを解消するために、「インターフェースとクラスの中間」の存在を考えてみましょう。
今回問題となったのは、インターフェースの「メソッドに処理を書けない」という部分です。
そこで、これを少し緩和して「メソッドに処理を書いても書かなくてもいい」としたものを 抽象クラス と呼ぶことにします。
インターフェース(抽象的)とクラスの中間ということが表現されていますね。
この概念を使うと、「平面図形抽象クラス」を「area
メソッドの処理は書かないが、volume
メソッドでは0
を返す」ように定義できます。
そして、一般のクラスの紐づけをインターフェースから抽象クラスに変更します。
このように、抽象クラスはインターフェースとクラスという両極端な存在の橋渡し的な立ち位置です。
処理を書くメソッド/書かないメソッドの割合も場合によって変化し、グラデーションがあります。
さて、この抽象クラスをプログラム上で書くことを考えると、インターフェースのときと同様に何かしらの「マーク」を付けておくのが良いでしょう。
「抽象」は英語で"abstract"なので、抽象クラスは"abstract class"と書くのが良さそうです。
また、名前についてもインターフェース同様、"Abstract"または"Base"(具体的なクラスの基礎となることから)を接頭辞にすることが慣例となっています。
個人的には"Base"の方が短くて好きなので、今回はこちらを採用します。
interface IPlanarShape
abstract class BasePlanarShape
class Circle
class Triangle
class Rectangle
以上を図にまとめると、次のようになります。
面積は図形によって計算方法が違うので、クラスごとに処理を実装しています。
体積はどの図形でも0なので、抽象クラスで定義することにより処理の重複を防いでいます。
このようにインターフェースと抽象クラスを使うことで、共通処理と個別処理を柔軟に配置することができます。
3.3. Javaでの表現方法
さて、ここまでインターフェースと抽象クラスに「マーク」を付けて区別しよう、という話をしてきました。
interface IPlanarShape
abstract class BasePlanarShape
class Circle
class Triangle
class Rectangle
実は、上のマークをそのまま使っているのがJavaです。
3種の概念ごとにマークがあるので区別を付けやすいですね。
しかし、Pythonではこのような仕組みにはなっていません。
Javaでの表現方法を念頭に置いて、次節でPythonでの表現方法を見ていきましょう。
3.4. Pythonでの表現方法
インターフェース、抽象クラス、クラスの性質を改めて見てみましょう。
種別 | 性質 |
---|---|
インターフェース | メソッドの処理は書かない |
抽象クラス | メソッドの処理は書いても書かなくてもよい |
クラス | メソッドの処理を書く |
Pythonの設計者は考えました。
「抽象クラスの全メソッドの処理を書かなかった場合、それはインターフェースと同一視できるのでは?」と。
つまり、インターフェースを特殊な抽象クラスとみなすことで、この3種の概念を「抽象クラスとクラス」の2種に集約してしまったのです。
さて、インターフェースについて調べていると、「Pythonにはインターフェースがない」という記述を見ることがあります。
この記述は混乱を招きがちなので、ここで整理しておきましょう。
上の説明の通り、Pythonではインターフェースは抽象クラスに集約されたため、「プログラミング言語の機能としてインターフェースがあるか?」についてはNoとなります。
しかし「インターフェースの概念を表現できるか?」は別の問題で、抽象クラスを活用すればインターフェースも表現できるのでYesとなります。
4. インターフェースを作ろう
それでは、実際に平面図形インターフェースを作ってみましょう。
Pythonにおけるインターフェースは、abc
モジュールのABC
クラス(ABstract Class、抽象クラス)を継承することで定義します。
「ABC
クラスを継承する」ことが、Javaにおけるinterface
マークと同様の機能を果たしています。
また、メソッドの処理を書かないということを明示的に示すため、全てのメソッドに@abstractmethod
デコレータを付けます。
from abc import ABC, abstractmethod
class IPlanarShape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def volume(self) -> float:
pass
そして、平面図形抽象クラスに関しても同じくABC
クラスを継承します。
前節でみた通り、インターフェースも抽象クラスも同じ表現方法になっています。
しかしこれだけだとインターフェースと抽象クラスの繋がりを表現できないので、平面図形抽象クラスでは更に平面図形インターフェースを継承します。
from abc import ABC, abstractmethod
class BasePlanarShape(IPlanarShape, ABC):
def volume(self) -> float:
return 0.0
平面図形抽象クラスのarea
メソッド(継承したので平面図形インターフェースのarea
メソッドと同一)には処理を書いていません。
しかし、volume
メソッドについては0
を返すという処理を書いています。
処理を書く/書かないを柔軟に切り替えて表現できていますね。
最後に、平面図形の具体例である円、三角形、長方形などは、平面図形抽象クラスを継承することで表現します。
from abc import ABC, abstractmethod
import math
class Circle(BasePlanarShape):
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
class Triangle(BasePlanarShape):
def __init__(self, side_1: float, side_2: float, side_3: float) -> None:
self.side_1 = side_1
self.side_2 = side_2
self.side_3 = side_3
def area(self) -> float:
# ヘロンの公式
s = (self.side_1 + self.side_2 + self.side_3) / 2
return math.sqrt(s * (s - self.side_1) * (s - self.side_2) * (s - self.side_3))
class Rectangle(BasePlanarShape):
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
平面図形抽象クラスで実装しなかったarea
メソッドを各平面図形クラスで実装しました。
これにより、平面図形インターフェースの全メソッドが抽象クラスとクラスの合わせ技により実装できました。
以上でインターフェースの構築は完了となり、場面に応じて適切なTypeHintを選べるようになります。
# 2つの平面図形の面積が同じか判定
def is_same_area(
shape_1: IPlanarShape, # 平面図形ならでれでもOKなので、インターフェースを記述
shape_2: IPlanarShape, # 平面図形ならでれでもOKなので、インターフェースを記述
) -> bool:
pass
# 三角形と長方形の外接円を返す
def circum_circle(
shape: Triangle | Rectangle, # 三角形、長方形に制限しているので、具体的なクラスを記述
) -> Circle: # 円を返すので、円クラスを記述
pass
5.まとめ
クラスとは極度に具体的なもので、全てのメソッドに対して処理を書かなければなりません。
一方でインターフェースは極度に抽象的なもので、メソッドの処理を書いてはならず、引数と戻り値しか定義できません。
この両者を橋渡しするのが抽象クラスで、処理を書くメソッドがあってもいいし、書かないメソッドがあってもいいです。
この三者の役割を理解することが、インターフェースを理解するための第一歩となります。
そして、PythonではJavaのような他言語と異なり、インターフェースと抽象クラスが統合されて扱われているということに注意しましょう。
ここを把握しておけば、インターフェースについて調べたときに混乱することはなくなるはずです。