sklearnに見る、pythonでのクラス継承 実践編

# はじめに
Pythonでクラス継承をどう書くのか？について実践編を書きます。
なお基礎編はありません。

## いつ重要？

ソフトウェア、大規模ライブラリを作る際に有用と思われます。
複数クラスで同じように定義するメソッドを、抽象クラスや鋳型となる親クラスに書いておけば子クラスで定義する必要がなくなります。

今回はみんな大好き scikit-learn の`TransformerMixin`を例に見ていきます。`TransformerMixin`とは、scikit-learnの随所で利用されているクラスです。scikit-learnでは前処理などで`fit`と`transform`を使うことが多く、それらをまとめて行う`fit_transform`が`TransformerMixin`に実装されています。

この機構により、を継承するだけで`fit_transform`が利用できるようになる、という非常にシンプルな例です。

参考までに、通常pythonでは`abc.ABCMeta`を継承した抽象クラスの実装が行われます。Pythonで常備されているパッケージにabcというものがあり、抽象クラスの定義に利用されます。本稿末尾の参考文献もご確認ください。

[abc — Abstract Base Classes](https://docs.python.org/3/library/abc.html)

# 試したこと

以下の2点について試しました。書いていきます。

1. ソースコードリーディング
2. サンプルコード動かした

## ソースコードリーディング

まず以下のコードを読んでみましょう。scikit-learnのgithubのページから引用しています。 [scikit-learn TransformerMixin](https://github.com/scikit-learn/scikit-learn/blob/e5698bde9/sklearn/base.py#L541)
 

```python:base.py
...
class TransformerMixin:
    def fit_transform(self, X, y=None, **fit_params):
        ...
        if y is None:
        ...
            return self.fit(X, **fit_params).transform(X)
        else:
        ...
            return self.fit(X, y, **fit_params).transform(X)
...
```

Pythonで抽象クラスを定義する場合よく使われるのは`abc.ABCMeta`というクラスです。
しかし、上の例では、`abc.ABCMeta`を使うこと無く、メソッドだけ定義されています。

`abc.ABCMeta`と`@abc.abstractmethod`を利用した抽象クラスでは、継承した先で実装を強要するクラスができます。
対して上記のTransformerMixinでは、共通のメソッドを全ての継承先で利用可能にしたいだけです。この場合は、`abc`は利用しなくて良いということです。


## サンプルコード動かした

試し書きしてみます。`abc`を使った抽象クラスを利用すると、以下の様に動作します。なお、関数名等はアベコベですので気にしないでください...

```python 
$ cat sampleAbsclass.py
from abc import ABCMeta, abstractmethod
class DensityMixin(metaclass=ABCMeta):
    @abstractmethod
    def score(self, X, y=None):
        pass

class OneClassSvm(DensityMixin):
    def __init__(self):
        print('echo echo', 'hello hello')
    # def score(self, X, y=None):
    #     print(*X.shape)

def main():
    ocsvm = OneClassSvm()
    from numpy.random import randn
    X=randn(100, 10)
    ocsvm.score(X)
    return 0

if __name__=='__main__':
    main()
$ python sampleAbsclass.py
Traceback (most recent call last):
  File "sampleAbsclass.py", line 21, in <module>
    main()
  File "sampleAbsclass.py", line 14, in main
    ocsvm = OneClassSvm()
TypeError: Can't instantiate abstract class OneClassSvm with abstract methods score
```

抽象クラスの`DensityMixin`内に`@abstractmethod`デコレーター付きのメソッド`score`があるのに、それを継承先の`OneClassSvm`で実装していないのは**ケシカラン！**というエラーメッセージです。

`OneClassSvm`のscore関数のコメントアウトを外せば下記のようにちゃんと動作します。

```python 
$ python sampleAbsclass.py
echo echo hello hello
100 10
```

## その他: abc.ABCMetaを使うときの注意点

他に、`abc.ABCMeta`を多用すると出てくる注意点として、クラスAが複数の抽象クラスを継承したとき、
「クラスAが1つでも`abc.ABCMeta`を継承した抽象クラスを有していれば、他の継承クラスも`abc.ABCMeta`を継承していないといけない」
という点があります。これを破ると以下のエラーメッセージが出ます。

```bash 
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
```

これは他の言語でも同じ現象が起きるので、そうすることでの利便性があるのだと思います。プログラミング言語の理論をちゃんと学ぶべきですね...

## その他2 abc.ABCMetaを使う例

scikit-learnで、`abc.ABCMeta`を利用している実装がありますのでそちらも見てみましょう。以下、[_PLS - scikit-learn](https://github.com/scikit-learn/scikit-learn/blob/1495f69242646d239d89a5713982946b8ffcf9d9/sklearn/cross_decomposition/pls_.py#L120)より引用します。`__init__`メソッドだけに`@abstractmethod`が追加されているのが分かります。

```python:pls_.py
from abc import ABCMeta, abstractmethod
...
class _PLS(BaseEstimator, TransformerMixin, RegressorMixin, MultiOutputMixin,
           metaclass=ABCMeta):
...
    @abstractmethod
    def __init__(self, n_components=2, scale=True, deflation_mode="regression",
                 mode="A", algorithm="nipals", norm_y_weights=False,
                 max_iter=500, tol=1e-06, copy=True):
...
    def fit(self, X, Y):
...
    def transform(self, X, Y=None, copy=True):
...
    def predict(self, X, copy=True):
...
    def fit_transform(self, X, y=None):
...
    def _more_tags(self):
        return {'poor_score': True}
```

この_PLSクラスを他のクラスが継承するのですが、このときだけABCMetaが出てきています。_PLSクラス自体はMixinばかりを継承しており、1番目の例から、ABCMetaクラスを継承したものは持ちません。

なので、`abc.ABCMeta`の用途としては繰り返しになりますが**複数クラスで必ず実装しないといけないメソッドがある場合に、抽象クラス内で継承する**というものが挙げられるでしょう。

(`TransformerMixin`を継承しているのに`_PLS`内で`fit_transform`を再定義しているのは謎です。)

# 結論

本記事ではscikit-learnの`TransformerMixin`の実装例を出発点として、共通部分をまとめるためのクラスをどの様に作るのか、というのを簡単にまとめました。
`abc.ABCMeta`クラスを継承して抽象クラスとする、というのは常套手段ですが、単に1, 2個のメソッドを複数クラスに共通して持たせたい場合は`abc.ABCMeta`を使うまでもなさそうであることを確認しました。
同じ実装を何度も繰り返してしまうのは愚かなので、うまくクラス継承を利用しましょう。

# 参考文献

`abc.ABCMeta` を使う方法は以下の記事に紹介されています。

- [Pythonの抽象クラスで色々試してみた - Qiita](https://qiita.com/Akira-Taniguchi/items/9e7520d7194176cbb60c)
- [Pythonで抽象クラスを作る方法!!](https://kiwamiden.com/how-to-create-abstract-classes-in-python)