Python でクラスキャスト

cast 関数

アップキャストなら、この実装である程度耐えられそう。

def cast(ParentalClass, child_object):
    child_object.__class__ = ParentalClass


アップキャスト
あるクラスBaseと、Baseから派生したクラスDerivedがあるとする。アップキャストとは、派生クラスから基底クラスへの型変換、すなわちDerivedのインスタンスをBaseに変換する操作である。
「DerivedのインスタンスはBaseのインスタンスである」ことは保証されているので、一般的にはこの変換は安全である。そのため、多くの言語において、これは暗黙に行うことができる。

動作確認と使用例

インスタンス変数は変化しませんが、クラス変数は変化します。

__class__ への代入の動作についてマニュアルの文言を見つけられなかったので、実際に触って期待した動作をするか動作確認しました。

class Person(object):
    def __init__(self, age, name):
        self.age = age
        self.name = name


class Student(Person):
    def __init__(self, age, name):
        super().__init__(age, name)
        self.grade = 1

    def rise_grade(self):
        self.grade += 1



Person クラスのオブジェクトを Student クラスにキャストしてみます。

>>> p = Person(15, 'Yaruo')
>>> cast(Student, p)
>>>
>>> #
>>> # __class__ 属性を変えても、インスタンス変数は変化しない。
>>> #
>>> p.grade
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'grade'
>>> 
>>> p.rise_grade()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in rise_grade
AttributeError: 'Student' object has no attribute 'grade'
>>>
>>> #
>>> # __class__ 属性を変えると、クラス変数は変化する。
>>> #
>>> p.grade = 1
>>> p.rise_grade()
>>> p.grade
2
>>>

◯ __class__ への代入条件

条件があります。

__class__ への代入は、両方のクラスが同じ __slots__ を持っているときのみ動作します。
3.3.2.3.1. __slots__ を利用する際の注意

◯ __slots__ って何?

__slots__ は、クラス生成の時のメモリ消費を抑えたい時に使います。

デフォルトでは、クラスのインスタンスは属性を保存するための辞書を持っています。これは、ほとんどインスタンス変数を持たないオブジェクトでは領域の無駄です。大量のインスタンスを生成するとき、この記憶領域の消費量は深刻になり得ます。
3.3.2.3. __slots__



使い方は簡単でクラス変数 __slots__ にインスタンス変数で使用する変数名を list などのシーケンスで渡すだけです。
__slots__でオブジェクトに属性を追加できないようにする | Python Snippets

◯ __*__ への代入について

一般に動作を保証されていません。何故なら __*__ への代入は、マニュアルで記載されている通り動作の保証がなされていないからです。

このドキュメントで明記されている用法に従わない、 あらゆる __*__ の名前は、いかなる文脈における利用でも、警告無く損害を引き起こすことがあります。
2. 字句解析 — Python 3.6.3 ドキュメント



ただし、本稿では上記のマニュアルの「動作する」という文言を持って、動作するという判断をしています。また、最後に動作原理を簡単に説明します。詳細は別記事で説明しています。

__class__ への代入は、両方のクラスが同じ __slots__ を持っているときのみ動作します。
3.3.2.3.1. __slots__ を利用する際の注意

調べたこと

組み込み関数の中でクラスキャストをサポートするものはなさそう。

2. 組み込み関数 < Python 標準ライブラリ

検討したこと

__init__ で定義してあげると、もっと自然な方法になると思うのですが、例えば、つぎのような具合に。

child_object = ChildClass(parent_object)

python - How to convert (inherit) parent to child class? - Stack Overflow
 

ただし、それは、つぎの2つの理由から却下しました(。-`ω-)ンー
1) 本来の目的で __init___ が使えなくなる。(例えば、Django の models.Model クラスを継承したクラスには適用できません。)
2) クラスごとに __init__ を定義しないといけない。
python - Writing a __init__ function to be used in django model - Stack Overflow

でも組み込みの str, int クラス(型)は、そのようなインターフェイスを提供してくれています。

>>> # 整数 -> 文字列
>>> str(1)
'1'
>>> # 文字列 -> 整数
>>> int('1')
1
>>> # 浮動小数 -> 整数
>>> int(0.1)
0

なんでこんな動作をするの?

インスタンスオブジェクトは、下図のように属性 __class__ にクラスオブジェクトへの参照が代入されています。この属性 __class__ に別のクラスを代入して、型を切り替えているという訳です。


f:id:domodomodomo:20170406193028j:plain

詳しいことは、この記事で説明しています。