いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Pythonメモ-105 (動的にクラスを定義)(type(), types.new_class())

概要

滅多に使わないのですが、たまーに使いたいシーンが出てきたりするのが動的にクラス定義するやり方。

よく忘れるので自分用にメモです。

環境

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.4
BuildVersion:   18E226

$ python --version
Python 3.7.2

Python では 「全てがオブジェクト」

よく言われるこの言葉ですが、Python の場合はクラス定義することもオブジェクトを作ることと同じことになります。

なんのオブジェクトかっていうと、 type クラスのインスタンスとなります。

実際に実行してみるとすぐに分かるのですが

class MyClass:
    pass
type(MyClass)
Out[3]: type

m = MyClass()
type(m)
Out[5]: __main__.MyClass

というように、クラスに対して type() を呼び出すと type って教えてくれます。

type() を引数一つで呼び出すと、object.__class__ が呼ばれます。

なので

MyClass.__class__
Out[6]: type

となります。__class__ は、そのクラスインスタンスがどのクラスに属しているかを示します。

クラスインスタンスの属しているクラスは __class__ 、継承先は __bases__ でわかります。

MyClass.__bases__
Out[7]: (object,)

ちなみに type(type) ってすると type って表示されます。こういうところがPythonいいですよね。

typeクラスとobjectクラスの関係については、以下の記事がわかりやすいです。

postd.cc

このあたりの仕組みは、Pythonでメタプログラミングする際にとても重要な概念です。

でもまあ、あんまり使わないに越したことはないテクニックですよね。魔術みたいになるし・・・。

シンプルなのが一番です。

type()で動的クラス定義

で、type()さんの出番なのですが、この人、関数に見えますが実はクラスです。

ドキュメント見ると

docs.python.org

ちゃんとクラスって書いてあります。

コンストラクタが2パターンあって、引数一つのものがいつも使っているやつです。

引数一つだと、object.__class__ を返してくれます。

もう一つあって、こっちは3つの引数を受け取って、新しい型オブジェクトを作って返してくれます。

こちらを使うと動的にクラス定義できます。

3つの引数は、それぞれ「クラスの名前」「継承クラスのタプル」「属性の辞書」です。

こんな感じ

In [50]: DynClass = type('DynClass', (object,), dict(member1=10))
In [52]: d1 = DynClass()
In [53]: d1.member1
Out[53]: 10

当然、いつものクラス定義と同じことしているのでちゃんとインスタンスも生成されます。

In [54]: d2 = DynClass()
In [55]: d2.member1 = 20
In [56]: d1.member1, d2.member1
Out[56]: (10, 20)

サンプル

あまり、いいサンプルではないですが、type()types.new_classを使ってクラス定義してるものです。

個人的には、types.new_classを利用する方が多いですね。

"""
type() を利用して動的にクラスを生成するサンプルです.

REFERENCES:: http://bit.ly/2HSBbSG
             http://bit.ly/2HUeeP3
             http://bit.ly/2HWSYb8
             http://bit.ly/2I7IJBh
             http://bit.ly/2HSPZkd
"""
import numbers

from trypython.common.commoncls import SampleBase
from trypython.common.commonfunc import pr


class Sample(SampleBase):
    # noinspection PyPep8Naming,PyUnresolvedReferences
    def exec(self):
        # -------------------------------------------------
        # Python では、「全てがオブジェクト」である。
        # この「全て」には、クラスも含まれる。
        # クラスは type のインスタンスである。
        # つまり、通常行っているクラス定義は type インスタンスを
        # 作成することと同じ。
        #
        # クラスインスタンスが属しているクラスは
        # Class.__class__ で取得することが出来る
        #
        # 組み込み関数 type() は、 __class__ を呼び出している
        # (Pythonのドキュメントには 組み込み関数と記載されているが
        #  実際にはtypeはクラスである。)
        # -------------------------------------------------
        class MyClass1:
            pass

        pr('type(MyClass1)', type(MyClass1))  # => <class 'type'>

        # type() に type を渡すと type となる
        # つまり、type は type クラス自身に属している
        pr('type(type)', type(type))  # => <class 'type'>

        # typeクラスは引数が一つのものと三つのものが存在する
        # 一つのものが通常利用しているもの。三つのものを利用すると
        # 新しい型オブジェクトを作成することが出来る。
        # つまり、クラス定義をしているのと同じことになる。
        # noinspection PyUnusedLocal,PyTypeChecker
        def func1(s, val):
            if isinstance(val, numbers.Number):
                return val + 1
            return 0

        clsname = 'DynClass1'
        baseclasses = (object,)
        classdict = dict(method1=func1)

        # 動的にクラス定義し、インスタンスを生成
        DynClass1 = type(clsname, baseclasses, classdict)
        d1 = DynClass1()
        pr('d1.method1(10)', d1.method1(10))  # => 11

        # 追記:
        # type() を利用する他に types モジュールを利用する方法もある
        # types モジュールは「動的な型生成と組み込み型に対する名前」と
        # タイトルが付いているので、そのまま動的型生成に利用できる
        # 動的に型生成する場合は、 types.new_class() を利用する
        import types

        # types.new_class() に渡す引数の exec_body にはちょっと注意が必要。
        # この引数は「新規で作成されたクラスの名前空間を構築するためのコールバック」となっている
        # つまり、引数に 名前空間(イコール dict) を受け取り、更新した名前空間を返すようにする必要がある
        # 基本的には lambda ns: ns.update(classdict) のようになる。
        def update_ns(ns: dict):
            ns.update(classdict)

        clsname = 'DynClass2'
        DynClass2 = types.new_class(clsname, baseclasses, exec_body=update_ns)
        d2 = DynClass2()
        pr('d2.method1(11)', d2.method1(11))  # => 12

        # types.new_class() で メタクラスを指定する場合は kwds 引数に指定する
        class DynMeta(type):
            _count: int = 0

            def __call__(cls, *args, **kwargs):
                cls._count += 1
                return super().__call__(*args, **kwargs)

            @property
            def creation_count(cls):
                return cls._count

        clsname = 'DynClass3'
        kwds = dict(metaclass=DynMeta)
        DynClass3 = types.new_class(clsname, baseclasses, kwds=kwds, exec_body=update_ns)

        # 10 回 生成を繰り返してみる
        instances = [DynClass3() for _ in range(10)]
        pr('DynClass3.creation_count', DynClass3.creation_count)  # => 10

        # 10 回 メソッドを呼び出してみる
        _ = [instance.method1(index) for index, instance in enumerate(instances)]
        pr('DynClass3.creation_count', DynClass3.creation_count)  # => 10


def go():
    obj = Sample()
    obj.exec()


if __name__ == '__main__':
    go()

try-python/dynamicclass01.py at master · devlights/try-python · GitHub

結果は以下のようになります。

type(MyClass1)=<class 'type'>
type(type)=<class 'type'>
d1.method1(10)=11
d2.method1(11)=12
DynClass3.creation_count=10
DynClass3.creation_count=10

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com