Python の メタプログラミング (__metaclass__, メタクラス) を理解する
Pythonのメタプログラミング (__metaclass__) は組み込み関数 type
の普段利用しない隠れた機能や、 普通は利用しない特殊メソッド __new__
などを理解する必要があり 理解するのが結構難しい。
あまり関連情報がまとまってるドキュメントがなくて理解するのに苦労したので情報をまとめておきました。
目次
事前知識
Customizing class creation
(日本語:クラス生成をカスタマイズする)
を読むと、型を取得するのに普通利用するbuiltin関数 type
を継承していたり、 普通利用することのない __new__
が定義されていたりして、 type
の隠された機能と __new__
について理解していないと
何が書かれているかさっぱり分からないと思います。 まずは __metaclass__
を理解する上で重要なこの2つについて整理しておきましょう。
type とクラス定義のあまり知られていない関係
ご存知のように Python には type
という builtin
関数が定義されています。 type は type(obj)
のように1つの引数を与えて
obj の型を取得するのに利用したことがあるはずです。
普通はこちらの機能しか使いません。
しかし実は type にはもう一つの隠れた機能があります。 第1引数に文字列でクラス名、第2引数に親クラスの列、第3引数にクラスのメソッドや属性を定義した dict を渡して type を呼び出すとクラスを動的に定義することが可能です。
class ClassName(P0, P1):
attrivute1 = value1
def function1(self, args):
...
のように普段クラスを定義していると思いますが、これは type
を使うと:
def function1(self, args):
...
ClassName = type('ClassName',
[P0,P1,],
{'attribute1': value1,
'function1': function1,})
のように定義することも可能です。この2つの定義は全く同じ実行結果が得られます。
ここで2つ目の type を使った定義をよく見てみると、 実は ClassName は type
クラスにクラス名、親クラス、クラス定義を渡してインスタンス生成
したものだということが分かります。クラスは type のインスタンス
なのです。 その証拠に type(cls)
は type
を返しますし、
isinstance(cls, type)
は True を返します。
特殊メソッド __new__
Python
でクラスを定義する際、インスタンスを初期化するメソッドとして普通は
__init__
を定義します。 厳密には Python
には「コンストラクタ」という用語はありませんが、 C++/Java
におけるコンストラクタに相当する処理は普通 __init__
に書かれます。
しかし実は Python にはインスタンスの生成方法を定義するもう一つの
特殊メソッド
__new__
が存在します (__new__
の日本語のドキュメント)。
Python でクラスを定義して、それを呼び出してインスタンスの生成を行うと
__init__
が
暗黙的に呼び出されインスタンスの初期化が行われると理解していると思いますが、
実はその前に __new__
による処理が存在しています。
クラスのインスタンス生成を行った際に暗黙的に行われている処理はより正確に書くと
class ClassName
がClassName()
によってインスタンス生成された場合- まず
ClassName.__new__
が第1引数に ClassName, 残りの引数に ClassName に与えた残りの引数が与えられて呼び出される。 - 普通は
__new__
は定義されていないので親クラスをたどっていって最終的にobject.__new__
が呼び出される。object.__new__
は第1引数で与えられた クラスのインスタンスを生成して返す。object.__new__
が返すインスタンスは__init__
が実行される前の未初期化のインスタンスである点に注意。 __new__
が ClassName のインスタンスを返した場合に限りClassName.__init__
が呼び出される。
というようになっています。 例えばものすごく極端な例ですが:
class C(object):
def __new__(cls, arg):
return str(arg)
def __init__(self):
print '__init__'
c = C(43)
print type(c)
のようなコードを書くと、c
には '43'
が代入されます。 そして
__init__
は呼び出されません。
__new__
の役割はかなり理解しづらいと思うので
よくドキュメントを読んでサンプルを書いて動かしていろいろ試してみたほうがよいかと思います。
個人的には ClassNameを呼び出すと __init__
が暗黙的に呼び出されるという
先入観が強すぎるせいか、 __new__
で何が制御できるのか理解するのがなかなか大変でした。
__metaclass__
class を定義すると自動的に type('ClassName', ...)
が呼び出されてクラスが生成されるということを type の節で述べました。
実は Python ではこの class
を定義される際に暗黙的に呼び出される関数を別の関数で置き換えることができます。
これが
__metaclass__
です (__metaclass__
の日本語のドキュメント)。
classの定義に __metaclass__
が存在するとクラスを生成する際に
__metaclass__
に格納された関数が type
の代わりに呼び出されます。
metaclass という名前がついていますが、__metaclass__
は class
である必要はありません。 type
と同様の引数を受け取れる callable
なオブジェクトならば何でも __metaclass__
として利用できます。 例えば
def tolower_classname(name, bases, dict):
return type(name.lower(), bases, dict)
class ClassName(object):
__metaclass__ = tolower_classname
print ClassName.__name__ # classname
のようなコードを書くと、ClassName の名前が 'classname' になります (classname というクラスが ClassName という変数に格納されている状態)。
typeの継承
ただそれだと metaclass
という名称とマッチしないので、実際にはtypeを継承したクラスを作成してそれを
__metaclass__
に指定するのが一番自然なのではないかと思います。
その場合、 __metaclass__
に指定されたクラスのインスタンスとしてクラスが作成されるようになるので、
クラス作成をカスタマイズするには __metaclass__
に指定したクラスの
__new__
もしくは __init__
をカスタマイズすることになります。
type
は __init__
ではなく __new__
メソッドの方でクラス生成の主な作業を行っています (typeobject.c
ソースコード)。
そのため、 type
の挙動をカスタマイズするには、普段オーバーライドする
__init__
ではなくて、 __new__
メソッドをオーバーライドする必要があります。 そして type.__new__
を呼び出す前に引数の name
, bases
, dict
を編集してクラス生成をカスタマイズすることになります。
class mymeta(type):
def __new__(cls, name, bases, dict):
# TODO: customize name, bases, dict.
type.__new__(cls, name, bases, dict)
メタクラスの例
__metaclass__
でどんなことができるのか理解するには例をみてみるのが一番だと思うので、
メタクラスのサンプルとして、__metaclass__
に指定すると getter/setter
っぽい名前のメソッド (e.g. get_name
, getName
, SetName
) を自動的に
プロパティ
に変換してくれるメタクラス auto_property
(ソースコード)
を作成しました。 例えば get_x
というメソッドを持つクラスに指定すると、アクセスすると get_x
が呼び出されるプロパティ x
が自動的に生成されます。 (逆に get_x
はメソッドから消えます)
class C(object):
__metaclass__ = auto_property
def get_x(self):
return 123
c = C()
assert c.x == 123
実装の解説
まずは type
を継承したクラス auto_property を定義します。 そして
__new__
の中で引数として受け取ったクラス定義の辞書 dict
をカスタマイズしてから、 type.__new__
を呼び出します。
class auto_property(type):
def __new__(cls, classname, bases, dict):
# TODO: dict から setter/getter っぽい名前のメソッドを取り除いて、
# 代わりに対応する property を持つ new_dict を作成する
return type.__new__(cls, classname, bases, new_dict)
後は、 TODO の部分で dict
に対して for文を回して正規表現を使って
getter/setter と property 名を取り出して、 最後に new_dict
に対して
property(getter, setter)
を代入するだけです。
class auto_property(type):
def __new__(cls, classname, bases, dict):
new_dict = {}
setters = {}
getters = {}
properties = set()
for name in dict:
value = dict[name]
# TODO: setter/getter を正規表現で検出
for property_name in properties:
getter = getters.get(property_name, None)
setter = setters.get(property_name, None)
new_dict[property_name] = property(getter, setter)
return type.__new__(cls, classname, bases, new_dict)
githubにソースコードをコミットしてある ので、実装の細かい部分はそちらを確認して下さい。