クラスをキャストする

以下のような具合に特殊属性 __class__ に代入すればいいだけです。

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

ここでは class 定義文を用いて作成したクラスをキャストする方法について検討します。

1. 動作確認と使用例

実際に触って期待した動作をするか動作確認しました。 Person クラスのオブジェクトを Student クラスにキャストしてみます。

def sample_code():
    person = Person(15, 'Yaruo')
    cast(Student, person)
    
    print('grade' in dir(person))  # False, grade は定義されていない。 
    person.grade = 1
    print(person.grade)   # 1
    person.raise_grade()  # Student クラスのメソッドを実行できる。
    print(person.grade)   # 2


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


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 raise_grade(self):
        self.grade += 1


if __name__ == '__main__':
    sample_code()

2. 調べたこと

2.1. 組み込み関数の中にあるか?

なさそう。 組み込み関数の中でクラスキャストをサポートするものがあるか見てみました。 しかし、なさそうでした。

2.2. class への代入条件

条件があります。

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

2.3. slots って何?

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

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

使い方は簡単でクラス変数 __slots__ にインスタンス変数で使用する変数名を list などのシーケンスで渡すだけです。

2.4. __*__ への代入について

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

このドキュメントで明記されている用法に従わない、 あらゆる __*__ の名前は、いかなる文脈における利用でも、警告無く損害を引き起こすことがあります。
2.3.2. 予約済みの識別子種 (reserved classes of identifiers)

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

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

2.5. クラスを変更することについて

基本的には、「良いアイディアではない」らいしいです。

identity のようにオブジェクトの class は、変更できません。[1]
[1] しっかりと管理された状況下においてかつ、いくつかの場合においては、オブジェクトの class を変更することができます。しかしながら、一般には良いアイディアではありません、何故なら、正しく取り扱われなかった場合、とても奇妙な挙動を引き起こすことになるからです。

Like its identity, an object’s type is also unchangeable. [1]
[1] It is possible in some cases to change an object’s type, under certain controlled conditions. It generally isn’t a good idea though, since it can lead to some very strange behavior if it is handled incorrectly.

3.1. Objects, values and types - The Python Language Reference

2.6. なんで「良いアイディアではない」の?

キャストにも色々種類があるらしいです。アップキャストなら安全だけど、ダウンキャスト、クロスキャストは、安全ではないらしいです。その変のことをちゃんと考えてなってことなのかな。詳しいことは Wikipedia をみてください。

型変換 - Wikipedia
アップキャスト
一般的にはこの変換は安全である。そのため、多くの言語において、これは暗黙に行うことができる。
ダウンキャスト
この変換は一般に安全ではなく、エラーが発生する可能性がある。そのため、多くの言語では明示的な構文が必要である。
クロスキャスト
ダウンキャストと同様に安全な型変換ではない。

3. 検討したこと

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

child_object = ChildClass(parent_object)

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

3.1. 静的キャスト

int ↔︎ str などのキャストは、上のようなやり方でインターフェイスが定義されています。どのようにしてクラスごとに、 init, new を定義しているのでしょうか?

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

型変換 - Wikipedia
静的キャスト
整数どうしの型変換や整数と浮動小数点数との間の型変換などの、ごく一般的な型変換。

3.2. 静的キャストでの実装

例えば str にキャストする際に、定義する場所は、 各クラスの __str__ メソッドです。

class Person:
    def __init__(self, name):
        self.name = name
    # キャストのされ方を定義する。
    def __str__(self):
        return self.name

person = Person('Yaruo')
# キャストする。
str(person)  # 'Yaruo'

str はインスタンス化された時に、キャスト対象のオブジェクトの __str__ メソッドを呼び出すだけです。

# たぶん、こんな感じで str は実装されてる。
# CPython のコードは確認していません。
class str(object):
    @staticmethod
    def __new__(obj):
        return obj.__str__()

もし型ごとに、ある1つクラスへのダウンキャスト、クロスキャストの仕方を複数のクラスに対して、定義したいような場合は、str と同じようにオブジェクトごとに変換メソッドを定義して、それを呼び出してあげてもいいのかなと思ったりもします。

4. 仕組み

cast 関数は、なんでこんな動作をするのでしょうか? インスタンスオブジェクトは、下図のように属性 __class__ にクラスオブジェクトが代入されています。cast 関数は、 この属性 __class__ に別のクラスを代入して、型を切り替えていたという訳です。

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

Last Updated: 5/24/2019, 2:47:11 AM