2014年12月15日月曜日

Pythonのメタプログラミング (メタクラス)

メタクラスを簡単に説明すると,「本来コードを書かなければ実現できないような処理を黒魔術的な処理でなんとかしちゃう」ためのテクニックです。コード量を(時には劇的に)減らすことができたり,すっきりした見通しの良いクラス設計を実現できます。

JavaScriptのPrototypeのような継承が可能なPythonのクラスを作ることにしましょう。Pythonのクラス設計とJavaScriptのそれの間には異なる部分があるので,魔法を使ってこの差を埋める必要があります。

まずはコードを見てみましょう。これがPythonでJSのPrototypeを実現するメタクラスです。コードをproto.pyというファイル名で保存しておきましょう。

#!/usr/bin/env python
## -*- coding: utf-8 -*-

class PrototypeStore(dict):
""" x.prototype.XXXの値を保存するためのクラス """
def __setattr__(self, name, value):
self[name] = value

def __getattr__(self, name):
return self[name]

class PrototypeMeta(type):
""" Prototypeメタクラス(クラス生成時に呼ばれる) """
def __new__(metacls, cls_name, bases, attrs):
cls = type.__new__(metacls, cls_name, bases, attrs)
cls.prototype = PrototypeStore()
return cls

class Prototype(object):
__metaclass__ = PrototypeMeta

def __getattr__(self, name):
if name == 'prototype':
getattr(self.__class__, name)
else:
try:
getattr(object, name)
except AttributeError:
return self.__class__.prototype[name]

class TestClass(Prototype):
def __init__(self):
pass

ProtoMetaの__new__()メソッドとPrototypeの__metaclass__アトリビュートがキモ。
objectを継承した新スタイルクラスに__metaclass__というアトリビュートが設定されていると,クラスは特別な動きをします。クラス生成時に,__metaclass__のアトリビュートに設定されたクラスを起動し,__new__()メソッドを呼び出すのです。

__new__()メソッド内部を見ると,type.__new__()メソッドを呼び出してクラスを生成したあと,クラスのプロパティにprotopypeというアトリビュートを代入していることが分かります。代入されているのはProtoStoreクラスのインスタンス。ProtoStoreクラスは辞書型を継承したクラス。__setattr__()と__getattr__()を継承していて,アトリビュートの読み書きを辞書のキー操作に置き換え,任意の名前を持つアトリビュートを定義,値を保存して読み出せるオブジェクトが作れるようになっています。

type.__new__()で作ってクラスアトリビュートを割り当てられたクラスは,Prototypeクラス,およびそのサブクラスの実体として機能します。

インタラクティブシェルで「from proto import *」して,次のコードを実行してみましょう。JSのPrototypeの挙動をエミュレートした,期待通りの動きになっていることを確認します。

first = TestClass() # オブジェクトを作る
first.prototype.x = 7 # 'x'をprototypeに割り当てる
second = TestClass() # firstと同じTextClassからインスタンスを作る
print second.x # first.xと同じオブジェクトを指しているので7になる
first.x = 9 # first(インスタンス)の'x'アトリビュートに代入
print first.x # これは7でなく9を返す
del first.x # インスタンスのアトリビュートを消去
print first.x # prototype.xの返す7になるはず

禅問答

Q:Protytyoeクラスの__init__()で「self.prototype=PrototypeStore()」ってやればいいんじゃないの?
A:インスタンスごとにprototypeが定義されてしまって期待通りの動きにならない。

Q:クラスアトリビュートとして「prototype=PrototypeStore()」を定義すれば?
A:Prototypeクラス,および全てのPrototype由来のクラスで同じprototypeを共有することになり,同じく期待通りに動かない。すべてのサブクラスで「prototype=PrototypeStore()」ってやらないとならない。

Q:じゃあ__init__()で「self.__class__.prototype=PrototypeStore()」では?
A:それでもできるけど,Pythonではクラスを継承したときに__init__()が上書きされてしまうので,__init__()を定義したすべてのサブクラスで「super(XXX, self).__init__(...)」ってやらないといけない。面倒だしはまる原因になる。

0 件のコメント:

コメントを投稿