目次


Pythonでのメタクラス・プログラミング

オブジェクト指向プログラミングを次のレベルに進める

Comments

オブジェクト指向プログラミングのおさらい

まずは、OOPがどんなものだったかを、30秒でおさらいしておきましょう。オブジェクト指向プログラミング言語では、クラス を定義することができます。クラスが目的とするのは、関係するデータと振る舞いをひとつにまとめることです。これらのクラスは、親 の性質の一部または全部を継承することもできますが、独自の属性 (データ) やメソッド (振る舞い) を定義することもできます。このプロセスの最後では、クラスが、インスタンス (単にオブジェクト と呼ばれることもある) を生成するためのテンプレートの役割を果たすのが一般的です。同じクラスであってもインスタンスが違えば、通常、保持されるデータは異なるわけですが、形式は同じになります。たとえば、Employee オブジェクトのbobjane は、いずれも.salary.room_number を備えていますが、両者の部屋と給料は、それぞれ異なります。

Pythonなど、いくつかのOOP言語では、オブジェクトに内省的 (introspective) (反省的 (reflective) ともいう) な振る舞いをさせることができます。つまり、内省的なオブジェクトは、自分自身を記述できるということです。たとえば、インスタンスがどのクラスに属しているのか、そのクラスは、どんな祖先をもつのか、そのオブジェクトには、どんなメソッドや属性が用意されているのか、といったことについてです。イントロスペクションのおかげで、オブジェクトを処理する関数やメソッドは、どんなオブジェクトが渡されてきたのかに基づいて決定を行うことができるわけです。イントロスペクションの機能がない場合でも、関数がインスタンス・データに基づいて分岐することは、よくあります。たとえば、jane.room_number への道順とbob.room_number へのそれとは、両者が別々の部屋に住んでいるわけですから、異なります。イントロスペクションを利用できれば、たとえば、jane には.profit_share 属性があるとか、bob はサブクラスHourly(Employee) のインスタンスであるといったことから、bob は飛ばして、jane のもらうボーナス額を計算するといったことも安全に行うことができます。

メタプログラミングからの返答

上で概観したとおり、基本的なOOPシステムは、かなり強力なのですが、上の説明で触れなかったことが1つあります。Python (その他の言語) では、クラスは、それ自体がオブジェクトであり、受け渡しやイントロスペクションの対象とすることができるという点です。上記のように、オブジェクトはクラスをテンプレートとして生成されるわけですが、では、クラスを生成するときには、何がテンプレートの役割を果たすのでしょうか。答は、もちろん、メタクラス です。

Pythonには、最初からメタクラスが用意されてきましたが、メタクラスにかかわる仕掛けは、Python 2.2になって、外からかなりよく見通せるものになりました。たとえば、Pythonは、バージョン2.2になると、すべてのクラス・オブジェクトを生成するための特殊な (ほとんど隠ぺいされた) メタクラスを1個だけ用意する言語ではなくなりました。プログラマーは、原始的なメタクラスtype をサブクラス化することができるようになり、変異可能なメタクラスを使って、動的にクラスを生成することすらできるようになりました。もちろん、Python 2.2でメタクラスを操作できる ということが、その必要性を説明することにはなった訳ではありません。

それに、クラス生成を行うために、特別に用意したメタクラス (カスタム・メタクラス) を使う必要もありません。もう少し分かりやすいのが、クラス・ファクトリー という概念で、通常の関数は、関数本体内で動的に生成されたクラスを返すことができるというものです。従来のPythonの構文では、以下のような記述ができます。

リスト1. かつてのPython 1.5.2でのクラス・ファクトリー
Python 1.5.2 (#0, Jun 27 1999, 11:23:01) [...]
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> def class_with_method(func):
...     class klass: pass
...     setattr(klass, func.__name__, func)
...     return klass
...
>>> def say_foo(self): print 'foo'
...
>>> Foo = class_with_method(say_foo)
>>> foo = Foo()
>>> foo.say_foo()
foo

ファクトリー関数class_with_method() は、ファクトリーに渡されてきたメソッド/関数を内容とするクラスを動的に生成し、返します。クラス自体は、返される前に関数本体内で操作されます。new モジュールを使えば、以下のように、もっと簡潔な記述にできますが、クラス・ファクトリー本体内のカスタム・コードで行えるような選択的な記述は行えません。

リスト2. newモジュールでのクラス・ファクトリー
>>> from new import classobj
>>> Foo2 = classobj('Foo2',(Foo,),{'bar':lambda self:'bar'})
>>> Foo2().bar()
'bar'
>>> Foo2().say_foo()
foo

いずれの場合も、クラス (FooFoo2) の振る舞いは、コードとして直接記述されるのではなく、実行時に動的な引数を使って関数を呼び出すことによって生成されます。また、動的に生成されるのが単なるインスタンス ではなく、クラス そのものである点は強調しておく必要があります。

メタクラス: 問題を探すための解法?

メタクラスは、ユーザーの99%の人が考える以上に奥の深い仕掛けです。必要かどうかを思案しているようなものは、必要ないのです (本当にそれを必要としている人は、それが必要であることを確信しており、なぜ必要なのかなど説明の必要はありません)。-- Pythonの権威Tim Peters

(クラスの)メソッド は、普通の関数と同様、オブジェクトを返すことができます。したがって、その意味では、クラス・ファクトリーが、関数となりうるのと同様、クラスとなりうることは明らかです。とくに、Python 2.2+ では、そのようなクラス・ファクトリーとしてtype という特殊なクラスが用意されています。当然ながら、読者の皆さんは、type() を、Pythonの古いバージョンの組み込み関数の曖昧さを取り去ったものとして捉えるのではないでしょうか。幸い、昔のtype() 関数の振る舞いは、type クラスに残されています (つまり、type(obj) はオブジェクトobj の型/クラスを返します)。新しいtype クラスは、以下のように、これまでずっと関数new.classobj が提供してきたのと同様のクラス・ファクトリーの役割を果たします。

リスト3. クラス・ファクトリー・メタクラスとしてのtype
>>> X = type('X',(),{'foo':lambda self:'foo'})
>>> X, X().foo()
(<class '__main__.X'>, 'foo')

ただ、type は、今度は (メタ) クラスですので、以下のように自由にサブクラス化することができます。

リスト4. クラス・ファクトリーとしてのtypeの派生
            >>> class ChattyType(type):
...     def __new__(cls, name, bases, dct):
...         print "Allocating memory for class", name
...         return type.__new__(cls, name, bases, dct)
...     def __init__(cls, name, bases, dct):
...         print "Init'ing (configuring) class", name
...         super(ChattyType, cls).__init__(name, bases, dct)
...
>>> X = ChattyType('X',(),{'foo':lambda self:'foo'})
Allocating memory for class X
Init'ing (configuring) class X
>>> X, X().foo()
(<class '__main__.X'>, 'foo')

魔法のメソッドとして特殊な.__new__().__init__() がありますが、概念的には、他のクラスに用意されているものと同じです。.__init__() メソッドでは、生成したオブジェクトの設定を行うことができ、.__new__() メソッドでは、生成オブジェクトの割り当てをカスタマイズすることができます。もちろん、後者は広く使われるわけではありませんが、Python 2.2の新しいスタイルのすべてのクラスに (通常、継承されて、オーバーライドはされずに) 用意されています。

type の派生には、注意すべきことが1つあります。最初にメタクラスを扱う人は誰でもひっかかることです。メソッドへの第1引数は、通常、self ではなくcls と呼ばれています。というのも、メソッドが、メタクラスではなく、生成された クラスを操作の対象とするからです。実際、これに関して何も特別なことはありません。メソッドは、すべて、インスタンスに結合され、メタクラスのインスタンスはクラスであるということです。以下のように、普通の名前を使えば、このことは、もっとはっきりします。

リスト5. クラスのメソッドを生成されたクラスに結合する
>>> class Printable(type):
...     def whoami(cls): print "I am a", cls.__name__
...
>>> Foo = Printable('Foo',(),{})
>>> Foo.whoami()
I am a Foo
>>> Printable.whoami()
Traceback (most recent call last):
TypeError:  unbound method whoami() [...]

こうした驚くほど平凡な仕掛けによって、メタクラスの操作を簡単にすると同時に新しいユーザーには混乱をもたらす少し甘美な構文を使えるようにしています。拡張された構文には、いろいろな要素が含まれています。といっても、これらの新しい変異の解決順位 (resolution order) は、なかなか理解しにくい面があります。クラスは、祖先からメタクラスを継承することができます。このことは、メタクラスを祖先にもつこと と同じではないことに注意してください (これも、よく混乱するところです)。昔のスタイルのクラスの場合、グローバルな_metaclass_ 変数を定義すると、強制的にカスタム・メタクラスを使用することになっていました。しかしながら、カスタム・メタクラスを使って生成したいクラスに対しては、ほとんどの場合、_metaclass_ クラス属性を設定しますが、これが最も安全な方法でもあります。属性が後で (クラス・オブジェクトがすでに生成された後に) 設定されるのなら メタクラスは使用されないわけですから、この変数は、クラス定義そのものの中で設定する必要があります。たとえば、以下のようにです。

リスト6. クラス属性を使ってメタクラスを設定する
>>> class Bar:
...     __metaclass__ = Printable
...     def foomethod(self): print 'foo'
...
>>> Bar.whoami()
I am a Bar
>>> Bar().foomethod()
foo

魔法で問題を解決

上では、メタクラスの基礎を見てきたわけですが、実際にメタクラスを利用するには、もっと繊細な技術を理解する必要があります。メタクラスの利用がやっかいなのは、通常のOOP設計では、クラスがあまり大したことをやっていない ことにあります。クラスの継承構造は、データとメソッドをカプセル化し、パッケージ化するのには有効ですが、人が具体的に操作するのは、通常、インスタンスです。

プログラミング作業の中で、メタクラスが真価を発揮していると思われるのは、大きく分けて2つの面においてです。

1つは、多分こちらのほうが一般的なのでしょうが、クラスが行わなければならないことをプログラマーが設計時にはっきりと 把握していないという側面です。もちろん、ある程度は理解しているのでしょうが、細かい点には、後にならないと手に入らない情報によって決まるようなものもあります。「後で」とは、次の2つの場合に分けることができます。(a) アプリケーションがライブラリー・モジュールを使うときと、(b) 実行時に、ある種の状況が発生したときの2つの場合です。これは、よく「アスペクト指向プログラミング (AOP)」と呼ばれているものに似ている側面です。以下の例は、それを優美に示していると思います。

リスト7. 実行時のメタクラスの構成
% cat dump.py
#!/usr/bin/python
import sys
if len(sys.argv) > 2:
    module, metaklass  = sys.argv[1:3]
    m = __import__(module, globals(), locals(), [metaklass])
    __metaclass__ = getattr(m, metaklass)
class Data:
    def __init__(self):
        self.num = 38
        self.lst = ['a','b','c']
        self.str = 'spam'
    dumps   = lambda self: `self`
    __str__ = lambda self: self.dumps()
data = Data()
print data
% dump.py
<__main__.Data instance at 1686a0>

察しはつくと思いますが、これは、data オブジェクト (従来のインスタンス・オブジェクト) の割と一般的な説明を表示するアプリケーションです。しかし、以下のように、このアプリケーションに実行時 引数が渡されると、結果は、かなり違ったものになります。

リスト8. 外部のシリアライゼーション・メタクラスを追加する
% dump.py gnosis.magic MetaXMLPickler
<?xml version="1.0"?>
<!DOCTYPE PyObject SYSTEM "PyObjects.dtd">
<PyObject module="__main__" class="Data" id="720748">
<attr name="lst" type="list" id="980012" >
  <item type="string" value="a" />
  <item type="string" value="b" />
  <item type="string" value="c" />
</attr>
<attr name="num" type="numeric" value="38" />
<attr name="str" type="string" value="spam" />
</PyObject>

この例では、gnosis.xml.pickle のシリアライゼーション・スタイルを使用していますが、一番最新のgnosis.magic パッケージには、メタクラス・シリアライザーのMetaYamlDumpMetaPyPicklerMetaPrettyPrint も用意されています。さらに、dump.py 「アプリケーション」のユーザーなら、MetaPicklerを定義している任意のPythonパッケージの中から、好みのMetaPicklerを使用するようにすることもできます。こういう目的で適当なメタクラスを記述するとすれば、以下のようなものになるでしょう。

リスト9. メタクラスを使って属性を追加する
class MetaPickler(type):
    "Metaclass for gnosis.xml.pickle serialization"
    def __init__(cls, name, bases, dict):
        from gnosis.xml.pickle import dumps
        super(MetaPickler, cls).__init__(name, bases, dict)
        setattr(cls, 'dumps', dumps)

このコードが実現することで注目すべき点は、どんなシリアライゼーションが使用されるのかをアプリケーション・プログラマーが知る必要がなく、さらには、コマンド・ラインにシリアライゼーションが追加されるのか、それ以外の横断的な機能が追加されるのかについてすら知る必要がないということです。

おそらく、メタクラスの最も一般的な用途は、MetaPicklersの用途と同様、生成されるクラスで定義されるメソッドに対して、メソッドの追加、削除、名前の変更、置き換えを行うことでしょう。今回の例では、「もともとの」Data.dump() メソッドがData クラスが生成されるときに (したがって、それ以下のすべてのインスタンスで)、アプリケーションの外側から、別のものに置き換えられています。

魔法による問題解決法は他にもある

プログラミングを行うとき、クラスがインスタンスよりも重要な役割を果たすことの多い特別な場面があります。クラスを利用した宣言中心のフレームワークにgnosis.xml.validity があります。このフレームワークでは、妥当なXML文書についての一連の制約条件を表現するための「妥当性チェック・クラス」をいろいろと宣言します。これらの宣言は、DTDに記述されるものと非常に近いものになります。たとえば、「論文」は、以下のようなコードで設定することができます。

リスト10. simple_diss.py。gnosis.xml.validityのルール
from gnosis.xml.validity import *
class figure(EMPTY):      pass
class _mixedpara(Or):     _disjoins = (PCDATA, figure)
class paragraph(Some):    _type = _mixedpara
class title(PCDATA):      pass
class _paras(Some):       _type = paragraph
class chapter(Seq):       _order = (title, _paras)
class dissertation(Some): _type = chapter

コンポーネントが正しいサブエレメントを備えていないdissertation クラスのインスタンスを生成しようとすると、記述例外が発生します。個々のサブエレメントについても、同様です。引数を正しい型に「持ち上げる」ための曖昧でない方法が1つだけの場合、正しいサブエレメントは、単純な引数を使って作成されます。

妥当性チェック・クラスが、多くの場合 (内々に) 既存のDTDを基にしている場合でも、これらのクラスのインスタンスは、以下の例のように、自分自身を、飾りのないXML文書の断片として表示します。

リスト11. 基本的な妥当性チェック・クラスの文書生成
>>> from simple_diss import *
>>> ch = LiftSeq(chapter, ('It Starts','When it began'))
>>> print ch
<chapter><title>It Starts</title>
<paragraph>When it began</paragraph>
</chapter>

メタクラスを使って妥当性チェック・クラスを生成することで、以下のように、クラス宣言そのものからDTDを作成することができます (また、それと同時に、これらのクラスにメソッドを追加することもできます)。

リスト12. モジュールのインポートの際にメタクラスを適用する
>>> from gnosis.magic import DTDGenerator,\
...                          import_with_metaclass,\
...                          from_import
>>> d = import_with_metaclass('simple_diss',DTDGenerator)
>>> from_import(d,'**')
>>> ch = LiftSeq(chapter, ('It Starts','When it began'))
>>> print ch.with_internal_subset()
<?xml version='1.0'?>
<!DOCTYPE chapter [
<!ELEMENT figure EMPTY>
<!ELEMENT dissertation (chapter)+>
<!ELEMENT chapter (title,paragraph+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT paragraph ((#PCDATA|figure))+>
]>
<chapter><title>It Starts</title>
<paragraph>When it began</paragraph>
</chapter>

パッケージgnosis.xml.validity は、DTDや内部のサブセットについては何も知りません。そのような概念や機能は、gnosis.xml.validitysimple_diss.py のいずれにもまったく 変更を加えることなく、完全にメタクラスDTDGenerator によって導入されています。DTDGenerator は、それが生成するクラスに対して、自分自身の.__str__() メソッドを置き換えることはしません。それでも飾りなしのXMLの断片を表示することができるわけですが、メタクラスなら、そうした魔法のメソッドを簡単に変更できるというわけです。

メタの便利な点

gnosis.magic パッケージには、アスペクト指向プログラミングに利用可能なメタクラスのサンプルがいくつか用意されている他、メタクラスを扱うためのユーティリティーがいろいろと含まれています。その中で最も重要なのがimport_with_metaclass() です。この機能は、上の例でも利用しているのですが、サード・パーティーのモジュールをインポートできるようにしつつも、モジュールのすべてのクラスを、type ではなくカスタム・メタクラスを使って生成できるようにします。そのサード・パーティーのモジュールに新しい機能を加えたい場合、どんなものであれ、自分で作成したメタクラスで定義することができます (あるいは、まるごと、他のところからもってくることもできます)。gnosis.magic には、プラグ可能なシリアライゼーション・メタクラスもいくつか含まれています。他のパッケージでは、トレース機能、オブジェクトの永続性、例外の記録などといった機能が用意されていたりします。

以下のimport_with_metclass() 関数では、メタクラス・プログラミングのいろいろな性質が示されています。

リスト13. [gnosis.magic] からのimport_with_metaclass()
def import_with_metaclass(modname, metaklass):
    "Module importer substituting custom metaclass"
    class Meta(object): __metaclass__ = metaklass
    dct = {'__module__':modname}
    mod = __import__(modname)
    for key, val in mod.__dict__.items():
        if inspect.isclass(val):
            setattr(mod, key, type(key,(val,Meta),dct))
    return mod

この関数に特徴的なスタイルは、通常のクラスMeta が、指定のメタクラスを使って生成されているという点です。ただし、いったんMeta が祖先として追加されると、その子孫も、カスタム・メタクラスを使って生成されることになります。基本的に、Meta のようなクラスは、メタクラス生成器と 継承可能なメソッドの集合の両方 を備えることができます。その遺産の2つの側面は、直交的です。


ダウンロード可能なリソース


関連トピック

  • メタクラスについては、Ira R. Forman、Scott Danforthの共著Putting Metaclasses to Work (Addison-Wesley、1999年刊) という本が役に立ちます。
  • Pythonのメタクラスに関して言えば、Guido van RossumのエッセーUnifying types and classes in Python 2.2 も役に立ちます。
  • Tim Petersをご存じない? ご存じだと思いますよ。まず、Timのwikiページ を読み、news:comp.lang.pythonを定期的に読むようにしましょう。
  • Karl J. LieberherrのConnections between Demeter/Adaptive Programming and Aspect-Oriented Programming (AOP) もAOPを説明した文書です。
  • 課題指向プログラミング (subject-oriented programming) も、参考になることと思います。IBM Researchの研究者が作成したもので、基本的には、アスペクト指向プログラミングと同じことが書かれています。
  • 本稿で何度か触れたGnosisのユーティリティーは、Davidのサイトからダウンロードできます。
  • developerWorks のLinuxゾーンには、他にもLinux開発者向けの参考文献が多数掲載されています。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=230168
ArticleTitle=Pythonでのメタクラス・プログラミング
publish-date=02262003