動的クラス継承とかいう黒魔術

# はじめに
**最初に断っておきますが、Pythonでは公式で動的クラス継承をサポートしていません。ということは推奨されないということですので、基本的に使わないでください。** というか使わなくてもなんとかなるでしょう。
本記事の方法では一応普通のクラス継承と見た目変わらなさそうな動作をしますが、もしかしたら意図しない動作をする可能性も大いにあります。
その辺りの詳しい検証はしていませんので、使う場合は自分だけとか、ごく小規模なグループ内程度に留めておいてください。もちろん自己責任でよろしくお願いします。
また、そういった詳細についてご存知の方いらっしゃればご教授よろしくお願いします！


# 概要
動的クラス継承できたらなんか便利じゃね？ということが研究中にありましたので、できないかな〜といろいろ調べたところ、残念ながら求める記事には辿り着けず...しかしいろいろなサイトを参考にそれっぽいことを実現しましたので覚書しておきます。
多重継承とはまた違いますのでご注意ください。(Pythonの多重継承もなかなか闇が深いみたいですね〜)


# 目次
- [クラスの継承](#クラスの継承)
- [動的クラス継承](#動的クラス継承)
    - [アトリビュートの継承](#アトリビュートの継承)
    - [メソッド・プロパティの継承](#メソッドプロパティの継承)


# クラスの継承
まずはクラスの継承について簡単に触れておきます。
クラスの継承はオブジェクト指向プログラミングにおける最大のメリットの一つで、使えるとコードがスッキリして見やすくなることが多いです。
![クラス継承.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/f444f472-ee60-41a7-46c0-5e2900e69038.png)
図で例えると、「動物」が**インターフェイス/抽象クラス**、「猫」「犬」が**親クラス**、それ以外が**子クラス**といった感じで名前がつけられています。

### インターフェイス/抽象クラス
インターフェイスや抽象クラスの違いについては[こちら](https://qiita.com/igayamaguchi/items/e1d35db0a14a84bda452)などをご覧ください。
Pythonでは`abc`というパッケージを利用すれば生成することができます。
正直インターフェイスと抽象クラスの違いについてはイマイチ理解できていない感が否めませんが、まあこれらを**継承したクラスに対して特定のメソッドなどを強制的に実装させる**、というのがキーポイントのようです。
**実装の設計図**みたいな理解でいいのではないでしょうか。先の図では「動物」がその役割を担っています。
例えば「鳴く」というメソッドを定義しておけば、子クラスで鳴き方の詳細を実装するように強制できます。

### 親クラス
インターフェイスや抽象クラスとは違い、子クラスにメソッドの実装を強制したりはできません。(`NotImplementedError`を投げておけば似たようなことはできます)
子クラス群に共通した処理を実装しておけば、子クラスでそのメソッドを実装する必要がなくなります。
例えば「猫」クラスに「グルーミング」というメソッドを実装しておけば、子クラスである「ペルシャ」などでいちいち「グルーミング」を実装する必要がなくなります。

### 子クラス
子クラスはインターフェイスや抽象クラス、親クラスを継承したクラスのことを言います。
図では「猫」「犬」「ペルシャ」「ジャーマンシェパード」などにあたります。
それぞれに固有のメソッドなどを実装することを目的としており、まあ詳細の実装をするためのクラスって感じです。


# 動的クラス継承
さて、本題である動的クラス継承の実装詳細について見ていきましょう。
とりあえずコード全文を載せます。

<details><summary>動的クラス継承コード全体</summary><div>

```dynamic_class_inheritance.py
from dataclasses import dataclass

import numpy as np


@dataclass
class Survive():
    heart_beat: int = 150

    def meow(self):
        print("ミャー")


@dataclass
class Dead():
    heart_bead: int = 0
    year_of_enjoyment: int = 5


@dataclass
class SchrodingerCat():
    threshold: float = 0.5
    
    def __post_init__(self, *args, **kwds):
        if np.random.rand() > self.threshold:
            obj = Survive()
        else:
            obj = Dead()
        
        # Add attributes to myself
        for k, v in obj.__dict__.items():
            if k not in self.__dict__:
                self.__dict__[k] = v
        # Keep them under the control of dataclass and rename my class name.
        my_fields = []
        for k, v in self.__dict__.items():
            if isinstance(v, (dict, list, set)):
                my_fields.append((k, type(v), field(default_factory=v)))
            else:
                my_fields.append((k, type(v), v))
        self.__class__ = make_dataclass(f"{obj.__class__.__name__}SchrodingerCat",
                                        my_fields, bases=(SchrodingerCat, obj.__class__))
        # Add funcgtions and properties to myself.
        for k in dir(obj):
            if k not in dir(self):
                setattr(self, k, getattr(obj, k))
                
    def hello(self):
        print("吾輩は猫である。名前はまだない。")


cat = SchrodingerCat()
print(cat.__class__)
print(f"{isinstance(cat, SchrodingerCat)=}")
print(f"{isinstance(cat, Survive)=}")
print(f"{isinstance(cat, Dead)=}")
print(cat)
pprint(fields(cat))
for x in dir(cat):
    if x[0] != "_":
        print(x)

cat.hello()
if isinstance(cat, Survive):
    cat.meow()
```
</div></details>

みんな大好きシュレーディンガーの猫に登場してもらいました。
![Schrodingers_cat.svg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/ea7a64e1-9045-9094-0d92-01efc8bfe83c.png)
(画像は[wikipedia](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%A5%E3%83%AC%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%AC%E3%83%BC%E3%81%AE%E7%8C%AB)より)
`__post_init__`にて確率で猫の生死が決まり、それを元にアトリビュートや関数の継承が行われます。
詳しく見ていきます。

## アトリビュートの継承
まずアトリビュートの継承から見ていきます。

<details><summary>アトリビュートの継承コード</summary><div>

```dynamic_class_inheritance.py
        # Add attributes to myself
        for k, v in obj.__dict__.items():
            if k not in self.__dict__:
                self.__dict__[k] = v
        # Keep them under the control of dataclass and rename my class name.
        my_fields = []
        for k, v in self.__dict__.items():
            if isinstance(v, (dict, list, set)):
                my_fields.append((k, type(v), field(default_factory=v)))
            else:
                my_fields.append((k, type(v), v))
        self.__class__ = make_dataclass(f"{obj.__class__.__name__}SchrodingerCat",
                                        my_fields, bases=(SchrodingerCat, obj.__class__))
```
</div></details>

オブジェクトのアトリビュートを取得するには`obj.__dict__`を利用します。この`__dict__`にはオブジェクトが持つ全てのアトリビュートが含まれており、**`obj`にあって`self`にないアトリビュートのみ`self`に追加するようにしてあります**。
もし双方が同じアトリビュートを持つ場合に`obj`の値へ上書きしたい場合は、`if`文を消してしまえばOKなはずです。

後半部分は`dataclass`を使用している場合にのみ必要な処理で、追加したアトリビュートを含めた全てのアトリビュートを持つ`dataclass`クラスを生成して`self.__class__`に上書きしています。
リスト型や辞書型、集合型は`field`を利用した時の宣言方法が異なるため場合分けしてあります。
また、`make_dataclass`の`bases`キーワードは親クラスの設定をしてくれるクラスです。
つまるところ、この部分での処理をまとめると、

1. フィールドを生成
2. `SchrodingerCat`と`obj.__class__`を継承した新しいクラスを生成
3. `self.__class__`に上書き

といったことをしています。

## メソッド・プロパティの継承
<details><summary>メソッド・プロパティの継承コード</summary><div>

```dynamic_class_inheritance.py
        # Add funcgtions and properties to myself.
        for k in dir(obj):
            if k not in dir(self):
                setattr(self, k, getattr(obj, k))
```
</div></details>

メソッドやプロパティの一覧を取得するには`dir`関数を利用します。(ちなみに実はアトリビュートも取得されますが、コードの読みやすさのためにわざと分けています)
ここでも**`obj`にあって`self`にないメソッド・プロパティのみ`self`に追加しています**。
もし上書きしたい場合はやはり`if`文を削除すればOKのはずです。ただ`__init__`などの特殊メソッドも上書きされてしまうので注意/工夫が必要です。


# おわりに
シュレーディンガーの猫って、それ自体は超有名ですが、実際きちんと知ってる人ってどのくらいいるんでしょうか？
ぼく自身も「猫かわいそう」くらいにしか思ってませんでしたが、実は結構奥が深い、面白い思考実験だと知って驚きました。量子力学の世界は摩訶不思議ですね〜


# 参考
- [python - フィールドをデータクラスオブジェクトに動的に追加する](https://cloud6.net/so/python/1242845)