Pythonで考える「インターフェース」

プログラミングについて調べごとをしていると、よく「インターフェース」という言葉が出てきます。
しかし、どういう意味だろうと思って調べてみても全然わからない......
加えてサンプルコードもJavaやC#ばかりで、Pythonのものはなかなかありません。
そこで今回はインターフェースについて、Pythonを使って考えてみたいと思います。


# 更新履歴
2025/06/06 : 初版
2025/06/07 : 文章の修正


# 環境
* Windows11
* Python3.12
* PyCharm Community Edition 2024.3.5


# 1. インターフェースの定義

インターフェースの一般的な定義は次の通りです。

> オブジェクト指向プログラミングにおいて、
> 複数の種類のオブジェクトを
> 多態性によって統一的に利用するため、
> 境界部分の汎用的な共通規格を定義したもの。
> [インタフェース (情報技術) / ソフトウェアインタフェース](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9_(%E6%83%85%E5%A0%B1%E6%8A%80%E8%A1%93))

いまいちピンとこないかもしれませんが、一番重要なのは

> 複数の種類のオブジェクトを統一的に利用する

の部分です。
つまり、インターフェースとは **「一般化」の概念を表現するための機能** と言い換えることができます。


# 2. インターフェースのモチベ

具体例で考えてみましょう。
ここに円(Circle)、三角形(Triangle)、長方形(Rectangle)クラスがあります。

```python
class Circle:
    pass


class Triangle:
    pass


class Rectangle:
    pass
```

これらの図形は全て面積を計算できます。
そこで、2つの図形の面積を比較して同じであれば`True`、違えば`False`を返す関数`is_same_area`を考えましょう。

```python
def is_same_area(
        shape_1: Circle | Triangle | Rectangle,
        shape_2: Circle | Triangle | Rectangle,
) -> bool:
    pass
```

しかし、このTypeHintの書き方には問題があります。
もし扱う図形が台形、平行四辺形、菱形、正方形......と増えていったら、際限なく長くなってしまいます。
そこで、これらの図形を一般化した「平面図形」を考えてみましょう。
これを使うことができたら、随分すっきりとした書き方ができそうです。

```python
def is_same_area(
        shape_1: 平面図形,  # 円も三角形も長方形もOK!
        shape_2: 平面図形,
) -> bool:
    pass
```

台形など扱う図形が増えたとしても、平面図形であれば同じように扱うことができます。
「複数の種類のオブジェクトを統一的に利用する」。
これはまさに、インターフェースの定義で触れた内容ですね。
そこで、「平面図形」を改めて「平面図形インターフェース」として考えることにしましょう。


# 3. インターフェースの表現方法

前節でインターフェースを使うメリットを見ましたが、考慮すべき問題があります。
それは「インターフェースをどう表現するか？」です。
言い方を換えれば「便利なのはわかるけど、プログラムでどう書けばいいのさ？」ということです。


## 3.1. インターフェースとクラスを区別したい！

これまで見てきた通り、インターフェースを「一般化」というキーワードで説明しました。
つまり、インターフェースにはいくつかの「具体例」となるクラスが紐づけられていると言えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/5ad6f18e-bdf5-48e7-99f7-66e0c466ec37.png)

プログラムを書くとき、インターフェースとクラスで全く同じ文法だとすると、折角性質を分けたのにどっちを指しているかわかりません。

```text:インターフェースはどれだ？
class PlanarShape
class Circle
class Triangle
class Rectangle
```

そこでプログラミング言語制作者は、インターフェースとクラスで文法そのものを変えよう、と考えました。
例えばインターフェースとクラスを区別する「マーク」を強制すれば、混同することはなさそうです。
更に、インターフェースは名前の先頭に"I" ("I"nterface)を付けることで、視覚的にも区別しやすくなりそうです。

```text:マークを付けた例
interface IPlanarShape
class Circle
class Triangle
class Rectangle
```

それだけではなく、インターフェースとクラスに明確な違いを設けることにしました。
それは、「インターフェースにはメソッドの中身（処理）を書くことができない」という制約です。
逆に何を書けるのかというと、「メソッドの名前、引数の変数名と型、戻り値の型」です。

インターフェースでは「メソッドの名前、引数、戻り値」に関するものだけを決めておき、紐づけられた具体的なクラスで処理を書く、という流れになります。
インターフェースを徹底的に抽象的なものとして定義したいという想いが伝わってきますね。

そして、「インターフェース」という単語が使われる理由も想像がつきます。
和訳すれば「境界面」。メソッドを使う人にとって、引数とは中身（処理）との入力側の境界面であり、戻り値とは中身（処理）との出力側の境界面に相当します。
これら「入出力の境界面（引数と戻り値）に関する取り決め（変数名や型）」という意味で、インターフェースという単語が使われているのです。
実際に、最初に触れたインターフェースの定義にも表れていますね。

> 境界部分の汎用的な共通規格を定義したもの。

「境界部分」というのはまさに境界面のことですから、「中身（処理）じゃなくて、境界面（引数と戻り値）の取り決めだよ」と言っている訳です。

以上を図にまとめてみると、次のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/8db12de0-e450-424a-9c9a-33a678455ee4.png)

平面図形インターフェースで`area`メソッドの引数と戻り値を定義し、円クラス、三角形クラス、長方形クラスではそれぞれの面積公式を使って面積を計算します。
このように、同じ`area`メソッドを使ったとしても中身の処理が異なる、というのを「多態性」と言います。


## 3.2. 困りごとと抽象クラス

インターフェースのメソッド内に処理を書かないという制約により、処理の流れをシンプルにすることができました。
しかし、これによって困りごとができてしまいました。
もし平面図形の体積を計算する`volume`メソッドを定義するとどうなるでしょうか？
平面図形の体積はどんな図形でも`0`なので、処理を書く場所はどこか一箇所で済ませておきたいところです。
しかし、平面図形を取りまとめている平面図形インターフェースには処理を書くことができません。
するとどうなるかというと、「`0`を返す」という処理を全ての平面図形クラスで実装しなければなりません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/09e1f0cd-b8e0-4f73-b85e-c4efc95db2f2.png)

これは面倒なことになりそうですね。
`volume`メソッドのように複数のクラスで共通した処理になる場合、同じ処理を何回も書くことになってしまいます。
でもインターフェースには処理を書きたくない......
そんな悩みを解消するために、「インターフェースとクラスの中間」の存在を考えてみましょう。

今回問題となったのは、インターフェースの「メソッドに処理を書けない」という部分です。
そこで、これを少し緩和して「メソッドに処理を書いても書かなくてもいい」としたものを **抽象クラス** と呼ぶことにします。
インターフェース（抽象的）とクラスの中間ということが表現されていますね。
この概念を使うと、「平面図形抽象クラス」を「`area`メソッドの処理は書かないが、`volume`メソッドでは`0`を返す」ように定義できます。
そして、一般のクラスの紐づけをインターフェースから抽象クラスに変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/a7e02bfd-0027-4173-beb6-4398cfcf7579.png)

このように、抽象クラスはインターフェースとクラスという両極端な存在の橋渡し的な立ち位置です。
処理を書くメソッド／書かないメソッドの割合も場合によって変化し、グラデーションがあります。

さて、この抽象クラスをプログラム上で書くことを考えると、インターフェースのときと同様に何かしらの「マーク」を付けておくのが良いでしょう。
「抽象」は英語で"abstract"なので、抽象クラスは"abstract class"と書くのが良さそうです。
また、名前についてもインターフェース同様、"Abstract"または"Base"（具体的なクラスの基礎となることから）を接頭辞にすることが慣例となっています。
個人的には"Base"の方が短くて好きなので、今回はこちらを採用します。

```text:"abstract"マークを付けた例
interface IPlanarShape
abstract class BasePlanarShape
class Circle
class Triangle
class Rectangle
```

以上を図にまとめると、次のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2573710/35b72a78-0084-4e4e-983c-2c1b2d8512d4.png)

面積は図形によって計算方法が違うので、クラスごとに処理を実装しています。
体積はどの図形でも0なので、抽象クラスで定義することにより処理の重複を防いでいます。
このようにインターフェースと抽象クラスを使うことで、共通処理と個別処理を柔軟に配置することができます。


## 3.3. Javaでの表現方法

さて、ここまでインターフェースと抽象クラスに「マーク」を付けて区別しよう、という話をしてきました。

```text
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`デコレータを付けます。

```python
from abc import ABC, abstractmethod


class IPlanarShape(ABC):

    @abstractmethod
    def area(self) -> float:
        pass

    @abstractmethod
    def volume(self) -> float:
        pass
```

そして、平面図形抽象クラスに関しても同じく`ABC`クラスを継承します。
前節でみた通り、インターフェースも抽象クラスも同じ表現方法になっています。
しかしこれだけだとインターフェースと抽象クラスの繋がりを表現できないので、平面図形抽象クラスでは更に平面図形インターフェースを継承します。

```python
from abc import ABC, abstractmethod

class BasePlanarShape(IPlanarShape, ABC):

    def volume(self) -> float:
        return 0.0
```

平面図形抽象クラスの`area`メソッド（継承したので平面図形インターフェースの`area`メソッドと同一）には処理を書いていません。
しかし、`volume`メソッドについては`0`を返すという処理を書いています。
処理を書く／書かないを柔軟に切り替えて表現できていますね。

最後に、平面図形の具体例である円、三角形、長方形などは、平面図形抽象クラスを継承することで表現します。

```python
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を選べるようになります。

```python
# 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のような他言語と異なり、インターフェースと抽象クラスが統合されて扱われているということに注意しましょう。
ここを把握しておけば、インターフェースについて調べたときに混乱することはなくなるはずです。