dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[python]メタプログラミングの基礎(__init__, __new__, __metaclass__)

python__init__, __new__, __metaclass__ や、それらを使ったメタクラスの作成方法などについて。
メタプログラミング」という言葉はたぶんこれ以外の機能についても言えるんだけど、参考にしたサイトが大体メタプログラミングという言葉を使っていたので、ここでもそうする。

こちらの記事が非常に参考になりました。
Python の メタプログラミング (__metaclass__, メタクラス) を理解する | yunabe.jp

細かい説明は上の記事に書かれているので、ここではポイントだけメモ。

ポイント

  • クラス定義は typeインスタンスである
  • MyClass() という書き方でクラスのインスタンスを生成したとき、内部では __new____init__ の順に呼ばれる
  • __new__ の内部では typeインスタンス化(type(cls, names, attrs) の呼び出し)が行われている
  • クラスに __metaclass__ という属性があると、typeインスタンス化のかわりに実行される

__metaclass__ の基本的な使い方としては:

  • type を継承したクラスAを定義する(メタクラス
  • 別のクラスBを定義し、メタクラスAをクラスBの __metaclass__ 属性に指定する
  • → クラスBがインスタンス化された時、クラスAの __new__ メソッドが呼ばれる。結果としてクラスBのインスタンス作成時の振る舞いをカスタマイズできる

以下、ポイントを順に見ていく。

クラス定義は typeインスタンスである

以下 (1) と (2) は同じ挙動となる。

# -*- coding: utf-8 -*-
class Foo(object):
    pass

# (1) 通常のクラス定義
class Bar(Foo):
    attr1 = 'bar'

    def method1(self, *args):
        print 'method1'

def func1(self, *args):
    print 'method1'

# (2) type を使ったクラス定義
# type(クラス名, 親クラス, 属性・メソッドの辞書)
Baz = type('Baz', (Foo,), {'attr1': 'baz', 'method1': func1})

bar = Bar()
bar.method1()
print type(Bar)  # True
print isinstance(Bar, type)  # True

baz = Baz()
baz.method1()
print type(Baz)  # True
print isinstance(Baz, type)  # True

ので、クラスを定義すると内部的には type('クラス名', ...) が実行されてクラスが生成されていることになる。

MyClass() という書き方でクラスのインスタンスを生成したとき、内部では __new____init__ の順に呼ばれる

__new__ の内部では typeインスタンス化(type(cls, names, attrs) の呼び出し)が行われている

python のクラスの初期化は通常 __init__ で行い、このメソッドを他の言語で言うところのコンストラクタのように利用することが多い。
が、インスタンス生成時には __init__ の前に __new__ が呼ばれている。

詳細な順番は http://www.yunabe.jp/docs/python_metaclass.html から引用させていただく。

  • class ClassNameClassName() によってインスタンス生成された場合
  • まず ClassName.__new__ が第1引数に ClassName, 残りの引数に ClassName に与えた残りの引数が与えられて呼び出される。
  • 普通は __new__ は定義されていないので親クラスをたどっていって最終的に object.__new__ が呼び出される。object.__new__ は第1引数で与えられた クラスのインスタンスを生成して返す。object.__new__ が返すインスタンス__init__ が実行される前の未初期化のインスタンスである点に注意。
  • __new__ClassNameインスタンスを返した場合に限り ClassName.__init__ が呼び出される。

また 公式ドキュメント__new__ についての説明から引用すると

クラス cls の新しいインスタンスを作るために呼び出されます。 __new__() は静的メソッドで (このメソッドは特別扱いされているので、明示的に静的メソッドと宣言する必要はありません)、インスタンスを生成するよう要求されているクラスを第一引数にとります。残りの引数はオブジェクトのコンストラクタの式 (クラスの呼び出し文) に渡されます。 __new__() の戻り値は新しいオブジェクトのインスタンス (通常は cls のインスタンス) でなければなりません。
(中略)
__new__() が cls のインスタンスを返した場合、 __init__(self[, ...]) のようにしてインスタンス__init__() が呼び出されます。このとき、 self は新たに生成されたインスタンスで、残りの引数は __new__() に渡された引数と同じになります。

__new__() が cls のインスタンスを返さない場合、インスタンス__init__() メソッドは呼び出されません。

クラスに __metaclass__ という属性があると、typeインスタンス化のかわりに実行される

先ほど、クラスを定義すると内部的には type('クラス名', ...) が実行されると書いたが、クラスに __metaclass__ という属性があるとこの内部的に実行される処理を別の関数で置き換えることができる。

__metaclass__ を利用するケースとしては、type を継承したクラスを用意し、別のクラスの __metaclass__ に指定するというのが一番自然である。

あまり実用的ではないけど、サンプル。

class MyMetaClasss(type):
    def __new__(cls, name, bases, attrs):
        attrs['foo'] = 'metacls was here'
        return type.__new__(cls, name, bases, attrs)

class MyClass(object):
    __metaclass__ = MyMetaClasss

    def bar(self):
        print self.__dict__

my_class = MyClass()
print my_class.foo  # 'metacls was here'

コードによってはメタクラスの第一引数は mcs と書いたり、attrs でなく dict と書いている。
また、__new__ 内では type.__new__ を呼んでいるコードと super(MyMetaClass, cls).__new__ というように親クラスを呼ぶように書いているコードと両方見られた。
(挙動は変わらないはず。変わるとしたらどういったケースか...?)

リファレンス

公式より

その他、参考にさせていただいた記事