1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-03-05

はじめに

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

概要

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

目次

クラスの継承

まずはクラスの継承について簡単に触れておきます。
クラスの継承はオブジェクト指向プログラミングにおける最大のメリットの一つで、使えるとコードがスッキリして見やすくなることが多いです。
クラス継承.png
図で例えると、「動物」がインターフェイス/抽象クラス、「猫」「犬」が親クラス、それ以外が子クラスといった感じで名前がつけられています。

インターフェイス/抽象クラス

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

親クラス

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

子クラス

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

動的クラス継承

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

動的クラス継承コード全体
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()

みんな大好きシュレーディンガーの猫に登場してもらいました。
Schrodingers_cat.svg.png
(画像はwikipediaより)
__post_init__にて確率で猫の生死が決まり、それを元にアトリビュートや関数の継承が行われます。
詳しく見ていきます。

アトリビュートの継承

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

アトリビュートの継承コード
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__))

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

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

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

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

メソッド・プロパティの継承

メソッド・プロパティの継承コード
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))

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

おわりに

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

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?