PEP-3115:Python 3000のメタクラス
メタクラスは、そのインスタンスもクラスになるクラスです。私はメタクラスをクラスカスタマイザと考えています。メタクラスはクラスデコレータと同じ役割を果たしますが、仕組みが異なります(より複雑です)。メタクラスとクラスデコレータには、次のような特徴の違いがあります。
- メタクラスは継承される。
- 各クラスは1つのメタクラスしか持てない。
このような単純な特徴でさえ、既に問題をはらんでいます。例えば、異なるメタクラスを持つ2つのクラスを継承したらどうなるのでしょうか。答えは、「場合による」です。2つのメタクラスに関連性がない場合、継承を掛け合わせることはできません。
class M1(type): pass class M2(type): pass class A(metaclass=M1): pass class B(metaclass=M2): pass class C(A, B): pass Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
これをうまく動かすには、一方のメタクラスをもう一方の(場合によっては間接的な)サブクラスにする必要があります。最派生メタクラスが、新しいクラスのメタクラスとして使われます。
class M1(type): pass class M2(M1): pass class A(metaclass=M1): pass class B(metaclass=M2): pass class C(A, B): pass >>> C.__class__ <class 'article_1.M2'>
Python 3.0のメタクラスを使うと、クラス属性の順序を__dict__からだけでは判断できないという、Python 2.xの厄介な問題が修正されます。多くの場合、外部システム(ORM、外国語ブリッジ、COMオブジェクトなど)と連携するときは順序が重要です。そのため、これらの用途にメタクラスを使用する場合は、すべての関連フィールドの順序付きリストを用意するなど、何らかの人為的な規則を使って順序を指定することを各クラスに義務付ける必要がありました。
>>> class A(object): ... x = 3 ... y = 4 ... z = 5 ... order = ['x', 'y', 'z'] ... >>> A.__dict__.keys() ['__module__', '__doc__', '__dict__', 'y', 'x', 'z', '__weakref__', 'order'] >>> A.order ['x', 'y', 'z']
これに対し、Python 3.0のメタクラスでは、標準のdict()の代わりにメンバを格納する働きをするオブジェクトを返す__prepare__()というメソッドを実装できます。__prepare__()が返すオブジェクトが、フィールドの順序を格納するか、イテレーションでは常に挿入された順序で要素を返すようにすることを利用します。
例えば、メソッドを決まった順序で呼び出す必要があるリニアワークフロークラスを作成するとします。いくつものワークフローがあり、ステップの追加と削除を続けていると、記録を付けて、ユーザーがワークフローのメソッドを正しい順序で呼び出すようにするためにかなりの注意を要することがあります。同じ状況でメタクラスベースの解決策を用いると、クラスで宣言したとおりの順序でメソッドを呼び出さなければならないクラスを作成できます。次のコードは、そのクラスと、クラスを正しく使った場合と間違って使った場合の例を示しています。
LinearWorkflowクラスは非常に単純で、start、step_1、step_2、step_3という4つのメソッドを持ち、メソッドはこの順序で宣言されています。また、このクラスはメタクラスを持ち(これについては後ほど詳しく説明します)、メソッドが正しい順序で呼び出されるようにしています。この例では、メソッドは単に自らの名前を出力します。クラスは次のようになっています。
class LinearWorkflow(metaclass=PedanticMetaclass):
def __init__(self):
pass
def start(self):
print('start')
def step_1(self):
print('step_1')
def step_2(self):
print('step_2')
def step_3(self):
print('step_3')
次の対話型セッションでは、LinearWorkflowオブジェクトをインスタンス化し、メソッドの呼び出しを開始しています。start()とstep_1()を呼び出し、それぞれのメソッドが実行されます。次に、step_1を再度呼び出そうとしますが、呼び出しの順序に反しているという理由で例外が発生します。step_2()の呼び出しに成功した後、強情にもstart()をもう一度(やはり順序に反して)呼び出そうとして、再び例外が発生しています。
>>> x = LinearWorkflow()
>>> x.start()
start
>>> x.step_1()
step_1
>>> x.step_1()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "article_1.py", line 192, in decorated
raise Exception('Method %s called out of order' % f.name)
Exception: Method step_1 called out of order
>>> x.step_2()
step_2
>>> x.start()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "article_1.py", line 192, in decorated
raise Exception('Method %s called out of order' % f.name)
Exception: Method start called out of order
この魔法にはどのような仕掛けがあるのでしょうか。PedanticMetaclassは、OrderedDict(挿入順にペアのリストとして要素を格納する辞書風クラス)を提供することによって、メソッドの順序の記録を付けています。PedanticMetaclassの__new__()メソッドを呼び出すと、修飾対象のクラス(この例ではLinearWorkflow)の、アンダースコアで始まらない各メソッドがOrderedDictに追加されます。その後、修飾対象のクラスで呼び出される各メソッドが記録され、メソッドが順序に反して呼び出されると例外が発生します。次にOrderedDictクラスとPedanticMetaclassクラスを示します。
class OrderedDict(collections.Mapping):
def __init__(self):
self._items = []
def __getitem__(self, key):
for k, v in self._items:
if k == key:
return v
raise KeyError(key)
def __setitem__(self, key, value):
for i, (k, v) in enumerate(self._items):
if k == key:
self._items[i] = (k, value)
return
self._items.append((key, value))
def __iter__(self):
return iter([k for k,v in self._items])
def __len__(self):
return len(self._items)
def __repr__(self):
s = '{'
for k, v in self._items:
s += '%s: %s, ' % (str(k), str(v))
s += '}'
return s
OrderedDictは、あくまで説明用の例にすぎません。非常に効率が悪いので、実稼働用のコードで使うことはお勧めしません。このクラスは要素にアクセスするたびにリニア検索を実行します。
PedanticMetaclassはあまり単純ではありませんが、コードを追うことはできるでしょう。__prepare__()メソッドはclassmethodでなければなりません。このメソッドは、単に空のOrderedDictを返します。decorate()関数は、各メソッド呼び出しを_orderリストで包んで、順序チェック処理のロジックを実行します。デコレータが__new__()メソッドで手動で適用された後、修飾対象のメソッドがsetattrを使ってnew_classに割り当てられます(元のメソッドを置き換えます)。
class PedanticMetaclass(type):
@classmethod
def __prepare__(metacls, name, bases):
return OrderedDict()
def decorate(self, f):
"""This method is called everytime an attribute is accessed
it ensures that the attributes are accessed in the order they were declared
"""
def decorated(*args, **keywds):
assert f.name in self._order
if self._called + [f.name] != self._order[:len(self._called) + 1]:
raise Exception('Method %s called out of order' % f.name)
self._called.append(f.name)
if len(self._called) == len(self._order):
self._called = []
return f(*args, **keywds)
return decorated
def __new__(metacls, name, bases, ordered_dict):
# Must convert the OrderedDict back to a regular dict
new_class = type.__new__(metacls, name, bases, dict(ordered_dict.items()))
# Create the ordered list of public methods automatically
# (ignore non-callables and methods that start with underscore)
order = [k for k,v in ordered_dict.items() if hasattr(v, '__call__')
and not k.startswith('_')]
new_class._order = order
# Keep the last accessed
new_class._called = []
# Decorate each method to record and check call order
for x in order:
m = getattr(new_class, x)
m.name = x
m = PedanticMetaclass.decorate(new_class, m)
setattr(new_class, x, m)
return new_class
