このサイトについて

Pythonのメタプログラミング (メタクラス) を理解したい人のための短いコード片と禅問答

Pythonのメタプログラミング (メタクラス) を理解したい人のための短いコード片と禅問答

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__(...)」ってやらないといけない。面倒だしはまる原因になる。

メタクラスを使えば,Prototypeクラスを継承するだけで期待通りの挙動が実現できる。余計なお約束のコードを書く必要がないので,楽だしクール:-)。

2010-10-14 05:51