Pythonの型ヒントでゆるくオブジェクト指向プログラミング - インターフェース・ポリモーフィズム・カプセル化

オブジェクト指向をPythonで実現するための記法をまとめます。特に、型ヒント (と `mypy`) を利用して静的解析を行う方法にフォーカスを当てています。

ただし、動的型づけ言語だと厳密に実現するのは難しいようです。厳密さにはあまりこだわらずに、メリットを受けられる範囲で (変更に柔軟に対応できるような) 実用的な書き方をまとめられたらなと思います。

今回は、こちら（[オブジェクト指向と10年戦ってわかったこと](https://qiita.com/tutinoco/items/6952b01e5fc38914ec4e)）の記事で取り上げられていたオブジェクト指向三大要素:

* 継承 (インターフェース)
* ポリモーフィズム
* カプセル化

を取り扱います。

オブジェクト指向そのものについての説明は上記を参考にして下さい。（まともに理解していないので説明できません。誤りがあればご指摘いただけるとありがたいです。）

現在も変更が多そうなライブラリ (`TensorFlow`, `PyTorch`, `Pandas`, Web framework (`Django`, `Flask`, `Starlette`, `FastAPI`など)) の実装方法を主観で整理した上でまとめたので、あまり突飛な実装方法は紹介していません。

今回使用したコードはすべて[github](https://github.com/tokusumi/python-oop)にあります。IDEでとりあえず試したい場合などはこちらからクローンしてみて下さい

## インターフェース

クラスの「規格」をまとめた抽象クラスの定義と、そのサブクラスが「規格」を準拠しているか検証する方法を整理します。（後述するポリモーフィズムと組み合わせると汎用性や変更のしやすさを向上させてくれます）

実装方法は、2パターンあります。

1. abc (Abstract Base Class)
1. NotImplementedError

### abc (Abstract Base Class)

`abc.ABC` や `abc.abstractmethod` デコレータを使用するとインターフェース (抽象クラス) が定義できます。また、この実装方法は、`mypy` と相性が良いです。

以下の様に Car インターフェース (抽象クラス) を定義できます:

```python
from abc import ABC, abstractmethod

class Car(ABC):
    @abstractmethod
    def move(self) -> bool:
        pass
    @abstractmethod
    def stop(self) -> bool:
        pass
```

これを継承したクラスは、少なくとも Car 抽象クラスで定義されている抽象属性と抽象メソッドを定義してあげる必要があります。(`abstractmethod` デコレータがついていない属性やメソッドは、通常通り"継承"されます。`abstractmethod` デコレータがついているメソッドも`.super()`でアクセスできます。)。つまり、抽象クラスで定義された「規格」に準拠しているか検証する機能が備わっています。

定義されていないものがある場合、以下の様にインスタンス生成時にエラーをはきます:

```python
class Tractor(Car):
    def move(self) -> bool:
        pass  # do something
    def stop(self) -> bool:
        pass  # do anything

tractor = Tractor()  # => OK

class Kar(Car):
    def move(self) -> bool:
        pass  # do something

kar = Kar()  # => TypeError
```

`mypy` で静的に解析することも可能です。(型アノテーションがなくても `mypy` で検出できます)

ただし、これはあくまでも未定義なメソッドを検知しているだけなので、メソッドの処理内容が「規格」に沿っているかテストするのを忘れてはいけないです。

#### 参考

* [Pythonの抽象クラス(ABCmeta)を詳しく説明したい](https://qiita.com/8128/items/7a76e46122191529c526)
* [PythonのABC - 抽象クラスとダック・タイピング](https://qiita.com/kaneshin/items/269bc5f156d86f8a91c4)

### NotImplementedError

以下の様に定義されたクラスをインターフェースとして使うこともできます:

```python
class BaseCar:
    def move(self) -> bool:
        raise NotImplementedError
    def stop(self) -> bool:
        raise NotImplementedError
```

このクラスを継承した以下のクラスは、`.stop()` メソッドの実行時にエラーをはきます:

```python
class Truck(BaseCar):
    def move(self) -> bool:
        pass  # do something

truck = Truck().stop()  # => NotImplementedError
```

`abc` を使う方法に比べると、

* どのクラスをインターフェースとして使う想定なのか分かりにくい
* `mypy` で、インターフェースで規定されているメソッドの定義不足を検出できない。(`.stop()` メソッドを実際に呼び出すまで不足していることが分からない)

という点で、使い勝手が悪いです。

しかし、テストを最低限やっておけばインターフェースとして一応機能すると思います。

### ライブラリでの使われ方

一般的に抽象クラス (または、単にベースクラス) 自体はよく使われています。

実装方法としては、主観ですが、`NotImplementedError` を `raise` するものが圧倒的に多いように感じます。

`abc` を使う場合も、汎用的なメソッドが詰め込まれたベースクラスに `abc` メタクラスを継承させてメインのメソッドだけ `abstractmethod` にしていることが多いです。

例えば:

* `Tensorflow.keras`: 至る所で Interface という表現と共に `abc` が使われています。例: Optimizer の基本機能 (抽象と具象の両方) が詰め込まれた `abc` メタクラスを定義し、それを継承して `SGD` や `Adam` など様々な Optimizer layer を定義している。 
* `Pytorch`: ベースクラス (`raise NotImplementedError`) が多く使われています。例: `torch.utils.data.Dataset` はベースクラスになっています。ユーザーがこれを継承してカスタムデータセットを定義する際に、インターフェースを使っていることになります。
* `Pandas`: 基本的にはベースクラスですが、`abc` 抽象クラスも要所要所で使われています。例: `Series.str` のメソッド群はそれぞれ共通の `abc` 抽象クラスをインターフェースとしています。
* Web Framework全般: `Django`, `Flask`, `Starlette`, `FastAPI` などはすべてベースクラス (`raise NotImplementedError`) が使われている模様です。

## ポリモーフィズム (多態性)

抽象クラス (インターフェース) に対してプログラミングします。そうすることで、抽象クラスを継承しているクラス（「規格」に準拠しているクラス）すべてが正常に動作することを期待できます。（各クラスのメソッドが期待通り正常に動作するという前提です）

具体的な処理の変更は具象クラスのメソッドの変更だけで閉じる可能性が高く、具体的な処理に依存しない処理 (抽象クラスで表現できる処理) の変更は個々の具象クラスについて気にする必要性が低くなります。変更の影響範囲が狭まれば狭まるだけ、より変更が容易になるので、ポリモーフィズムは重要そうです。

実装方法は、細かく分けると

1. 抽象クラス (インターフェース) として型アノテーション
1. `isinstance` で抽象クラス (インターフェース) と比較する

です。いずれにしても、`mypy` を利用します。

また、`abc` 抽象クラスとベースクラス (`raise NotImplementedError`) のどちらでインターフェースを定義しても同様に動作します。

### 抽象クラスを型アノテーション

抽象クラス (インターフェース) を型ヒントに使うと、実際にはそれを継承したクラスを代入していても、`mypy` 使用時には抽象クラスとして静的解析が行われます。なので、具体クラス特有のメソッドや属性の利用、インターフェースを使用していないクラスの誤った受け渡しを検知してくれます。

```python
from abc import ABC, abstractmethod


class Car(ABC):
    @abstractmethod
    def move(self) -> None:
        pass

class Taxi(Car):
    def move(self) -> None:
        pass

    def stop(self) -> None:
        pass


def stop_car(car: Car) -> None:
    # 抽象クラスで型ヒントをつける
    car.stop()  # => (mypy) error: "Car" has no attribute "stop"

stop_car(Taxi())
```

また、同一インターフェースを継承したクラス達を動的に切り替える可能性がある場合 (関数内で具体クラスのインスタンス生成をする必要があるとき) は、次の様に出力を抽象クラスでアノテーションした関数を用意しておくと、`mypy` が抽象クラスとして扱ってくれます:

```python
class Truck(Car):
    def move(self) -> None:
        pass

    def load(self) -> None:
        pass


def get_car(model: str) -> Car:
    if model == "taxi":
        return Taxi()
    else:
        return Truck()


def stop_car(model: str) -> None:
    # 出力が抽象クラスでアノテーションされている関数を用いる。
    car = get_car(model)
    car.stop()  # => (mypy) error: "Car" has no attribute "stop"
    # or 変数を抽象クラスでアノテーション
    car_ex: Car = get_car(model)
    car_ex.stop()  # => (mypy) error: "Car" has no attribute "stop"
```

### `isinstance` で抽象クラスと比較する

また、型ヒントを使わなくても、`isinstance` で抽象クラスを継承しているか確認すれば、`mypy` が抽象クラスとして解析してくれます。（実行時は、Carを継承したクラスのインスタンスを渡せば`isinstance`はTrueになるので問題なく関数が使えます。）

```python
def stop_car(car) -> None:
    if not isinstance(car, Car):
        raise TypeError
    car.stop()  # => (mypy) error: "Car" has no attribute "stop"
```

### ライブラリでの使われ方

これは、型アノテーションがどの程度採用されているか次第です。型ヒントのついているライブラリだと、素直にベースクラスをアノテーションしてポリモーフィズムを実現しています。

型ヒントがついていないライブラリだと、`mypy` は使っていないと思いますが、要所要所で `isinstance` でベースクラスを継承しているか確認してから処理を行っています。細かく確認しているわけではなく、内部的な使用を目的とした関数やメソッドは何の確認もしていない場合が多いです。

## カプセル化

いくつか見方があるようなので、整理すると:

1. 互いに関連するデータの集合とそれらに対する操作を一つにまとめる
1. 内部的に使用する属性/変数を外部から変更できないようにする
1. クラスの役割をひとつにする。(無駄を省き洗練された分かりやすいものを作る)

### 互いに関連するデータの集合とそれらに対する操作を一つにまとめる

とりあえず、`class` を使っておけばこれはクリアされていると思います。

### 属性/変数を外部から変更できないようにする

こちらは話が簡単です。Pythonはconstやprivateのようなものが言語仕様上ないので、Pythonで厳密に実現するのは不可能です。妥協しましょう。

ですが、`property` デコレータと `abc.abstractmethod` を使えば `mypy` で (ポリモーフィズムに則って、抽象クラスに対してプログラミングできている場合に限り)、以下の様に外部からの変更を静的解析できます:

```python
class Car(ABC):
    @property
    @abstractmethod
    def is_active(self) -> bool:
        pass

def assign(car: Car) -> None:
    # read-onlyな属性の変更を検知
    car.is_active = False  # => (mypy) error: Property "is_active" defined in "Car" is read-only
```

単に `property` デコレータを使って定義した場合は変更不可能な属性になります。しかし、この抽象クラスを継承したクラスでsetterを新たに定義すれば (これは禁止できません) 外部からの変更を許容してしまいます。ただし、ポリモーフィズムを利用して、関数内で抽象クラスとして扱えば、`mypy` の静的解析で、外部からの変更箇所を自動的に特定できます。

逆に、変更を許容したい場合は、setterの `abc.abstractmethod` を抽象クラスで定義しておけばいいです。

もう一つ、お作法として変数名の先頭に _ (アンダーバー), __ (dunder) をつける方法があります。しかし、多少の制約はありますが、どちらも外部からアクセスできてしまいます。
あくまでもお作法を知っていて、良識な開発者はこれらの変数の変更を避けてくれるかもしれないという程度のものです。

#### 参考

* [stackoverflow: What is the meaning of single and double underscore before an object name?
](https://stackoverflow.com/a/1301369)
* [初学者のためのPython講座　オブジェクト指向編7　カプセル化](https://qiita.com/kotakahe/items/2e17231868d78b35639f)

### クラスの役割をひとつにする

これは、記法ではなく設計の話なので本記事では扱いません。

### ライブラリでの使われ方

`property`の利用や、変数名にアンダーバーをつけているものは多数ありますが、それ以上に厳格な対策はしていなさそうです。

他の要素に比べると、カプセル化は妥協が必要だということだろうと思います。

## まとめ

Pythonは動的型付け言語なので、あまり厳密にオブジェクト指向プログラミングができるわけではないとはいえ、オブジェクト指向的な設計思想は色々なライブラリに採用されており、Pythonでも十分有用なのではないでしょうか。

型アノテーションと `mypy` のおかげで、いくつかの重要な要素はラフに試せるようになってきていると感じました。クラス自体が型なのでアノテーションが簡単な上に、今回紹介した方法はほとんど余計な型アノテーションが要らないので、あまり型ヒントを使っていない人にもおすすめです。

オブジェクト指向とまともに戦ったことはないので、適宜取り入れていきたいです。

今回は特に有名な三要素のみ扱いましたが、オブジェクト指向はもっと奥が深いようなので、さらに掘り下げてみるとより便利なものがあるかもしれません。

## 参考

* [オブジェクト指向と10年戦ってわかったこと](https://qiita.com/tutinoco/items/6952b01e5fc38914ec4e)
* [なぜオブジェクト指向は難しいのか](https://qiita.com/tutinoco/items/7f7568cc7dbf7e2276c8)
* [Pythonのオブジェクト指向プログラミングを完全理解](https://qiita.com/kaitolucifer/items/926ed9bc08426ad8e835)
* [Linting Python in Visual Studio Code](https://code.visualstudio.com/docs/python/linting)
* [今回使用したコード](https://github.com/tokusumi/python-oop)