はじめに
最初に断っておきますが、Pythonでは公式で動的クラス継承をサポートしていません。ということは推奨されないということですので、基本的に使わないでください。 というか使わなくてもなんとかなるでしょう。
本記事の方法では一応普通のクラス継承と見た目変わらなさそうな動作をしますが、もしかしたら意図しない動作をする可能性も大いにあります。
その辺りの詳しい検証はしていませんので、使う場合は自分だけとか、ごく小規模なグループ内程度に留めておいてください。もちろん自己責任でよろしくお願いします。
また、そういった詳細についてご存知の方いらっしゃればご教授よろしくお願いします!
概要
動的クラス継承できたらなんか便利じゃね?ということが研究中にありましたので、できないかな〜といろいろ調べたところ、残念ながら求める記事には辿り着けず...しかしいろいろなサイトを参考にそれっぽいことを実現しましたので覚書しておきます。
多重継承とはまた違いますのでご注意ください。(Pythonの多重継承もなかなか闇が深いみたいですね〜)
目次
クラスの継承
まずはクラスの継承について簡単に触れておきます。
クラスの継承はオブジェクト指向プログラミングにおける最大のメリットの一つで、使えるとコードがスッキリして見やすくなることが多いです。
図で例えると、「動物」がインターフェイス/抽象クラス、「猫」「犬」が親クラス、それ以外が子クラスといった感じで名前がつけられています。
インターフェイス/抽象クラス
インターフェイスや抽象クラスの違いについてはこちらなどをご覧ください。
Pythonではabc
というパッケージを利用すれば生成することができます。
正直インターフェイスと抽象クラスの違いについてはイマイチ理解できていない感が否めませんが、まあこれらを継承したクラスに対して特定のメソッドなどを強制的に実装させる、というのがキーポイントのようです。
実装の設計図みたいな理解でいいのではないでしょうか。先の図では「動物」がその役割を担っています。
例えば「鳴く」というメソッドを定義しておけば、子クラスで鳴き方の詳細を実装するように強制できます。
親クラス
インターフェイスや抽象クラスとは違い、子クラスにメソッドの実装を強制したりはできません。(NotImplementedError
を投げておけば似たようなことはできます)
子クラス群に共通した処理を実装しておけば、子クラスでそのメソッドを実装する必要がなくなります。
例えば「猫」クラスに「グルーミング」というメソッドを実装しておけば、子クラスである「ペルシャ」などでいちいち「グルーミング」を実装する必要がなくなります。
子クラス
子クラスはインターフェイスや抽象クラス、親クラスを継承したクラスのことを言います。
図では「猫」「犬」「ペルシャ」「ジャーマンシェパード」などにあたります。
それぞれに固有のメソッドなどを実装することを目的としており、まあ詳細の実装をするためのクラスって感じです。
動的クラス継承
さて、本題である動的クラス継承の実装詳細について見ていきましょう。
とりあえずコード全文を載せます。
動的クラス継承コード全体
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()
みんな大好きシュレーディンガーの猫に登場してもらいました。
(画像はwikipediaより)
__post_init__
にて確率で猫の生死が決まり、それを元にアトリビュートや関数の継承が行われます。
詳しく見ていきます。
アトリビュートの継承
まずアトリビュートの継承から見ていきます。
アトリビュートの継承コード
# 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_dataclass
のbases
キーワードは親クラスの設定をしてくれるクラスです。
つまるところ、この部分での処理をまとめると、
- フィールドを生成
-
SchrodingerCat
とobj.__class__
を継承した新しいクラスを生成 -
self.__class__
に上書き
といったことをしています。
メソッド・プロパティの継承
メソッド・プロパティの継承コード
# 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__
などの特殊メソッドも上書きされてしまうので注意/工夫が必要です。
おわりに
シュレーディンガーの猫って、それ自体は超有名ですが、実際きちんと知ってる人ってどのくらいいるんでしょうか?
ぼく自身も「猫かわいそう」くらいにしか思ってませんでしたが、実は結構奥が深い、面白い思考実験だと知って驚きました。量子力学の世界は摩訶不思議ですね〜