魅力的な Python: デコレーターで魔法を身近に

メタプログラミングのための最新 Python 機能についての考察

Python はメタプログラミングを可能にしましたが、メタプログラミングを実現する方法は、Python のバージョンごとに多少異なる (そして完全に互換するわけではない) 趣向が加えられています。Python の魔法に手を伸ばす手法として長いことあれこれ操作されてきたのは、ファースト・クラスの関数オブジェクトです。バージョン 2.2 では、Python はカスタム・メタクラスのメカニズムを拡張しました。これは効果を発揮しましたが、ユーザーの頭を混乱させるという犠牲を伴いました。最近ではバージョン 2.4 で「デコレーター (修飾子)」が拡張されています。デコレーターは、大抵のメタプログラミングを行う際の最新かつ、とびきりユーザー・フレンドリーな手段になります。

David Mertz, Ph.D. (mertz@gnosis.cx), Developer, Gnosis Software, Inc.

David MertzDavid Mertz は、2000年からのdeveloperWorks 記事、「魅力的な Python」と「XML の論考」を書いています。著書には『Text Processing in Python』もあります。David について詳しくは、彼個人の Web ページにアクセスしてください。



2006年 12月 29日

ほんの少しの手間で大きな効果を上げる

デコレーターには、Python に以前導入されたメタプログラミング抽象化との共通点があります。それは、デコレーターはそれなしでできないことは何もしないという点です。Michele Simionato と私が前に「魅力的な Python」の記事で指摘したように、Python 1.5 でさえも「メタクラス」フックなしで Python クラスの作成を操作することは可能でした。

いずれのデコレーターも、非常にありふれているという点で共通しています。デコレーターが行うことは、デコレーターの直後に定義された関数またはメソッドを変更することだけです。このような変更は今までも常に可能でしたが、デコレーター機能はとりわけ、Python 2.2 で classmethod() および staticmethod() という組み込み関数が導入されたことによって動機付けられました。かつてのスタイルでは、classmethod() 呼び出しは例えば以下のように使用されることになります。

リスト 1. 典型的な「古いスタイル」の classmethod
class C:
    def foo(cls, y):
        print "classmethod", cls, y
    foo = classmethod(foo)

classmethod() は組み込み関数ですが、それ自体に固有のものは何もないので、「独自」のメソッド変換関数を使うこともできます。以下はその一例です。

リスト 2. 典型的な「古いスタイル」のメソッド変換
def enhanced(meth):
    def new(self, y):
        print "I am enhanced"
        return meth(self, y)
    return new
class C:
    def bar(self, x):
        print "some method says:", x
    bar = enhanced(bar)

デコレーターが行うことは、メソッド名を繰り返さなくても済むようにすることだけなので、デコレーターはメソッド定義でメソッドの最初の記述の近くに書き込みます。

リスト 3. 典型的な「古いスタイル」の classmethod
class C:
    @classmethod
    def foo(cls, y):
        print "classmethod", cls, y
    @enhanced
    def bar(self, x):
        print "some method says:", x

デコレーターは通常の関数に対しても、クラス内のメソッドに対する場合と同じように機能します。このような単純で、厳密に言えば必要のない変更を構文に加えただけで、物事を改善することになり、プログラムについて容易に論理的な考え方ができるようになるだなんて、あまりの簡単さに驚いています。メソッド定義の関数の前に複数のデコレーターをリストすれば、デコレーターのチェーンを作ることもできます。あまりにも多くのデコレーターを連結するのは常識的に避けなければなりませんが、ある程度の数であれば賢明な場合もあります。

リスト 4. チェーン・デコレーター
@synchronized
@logging
def myfunc(arg1, arg2, ...):
    # ...do something
# decorators are equivalent to ending with:
#    myfunc = synchronized(logging(myfunc))
# Nested in that declaration order

デコレーターは単なる糖衣構文なので、のめりこみすぎると墓穴を掘るはめになることもあります。デコレーターは最低 1 つの引数を取る関数にすぎません。つまり、デコレーターが意味のある関数あるいはメソッドを返し、この関数やメソッドが、接続を有益にするために元の関数が行っていた内容を確実に実行するようにするのは、デコレーターのプログラマーの役目です。以下に、誤った構文の使用例をいくつか挙げてみましょう。

リスト 5. 関数を返すことさえしない不良デコレーター
>>> def spamdef(fn):
...     print "spam, spam, spam"
...
>>> @spamdef
... def useful(a, b):
...     print a**2 + b**2
...
spam, spam, spam
>>> useful(3, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'NoneType' object is not callable

以下のデコレーターは関数を返すかもしれませんが、返される関数には対応する修飾されていない関数との意味のある関連付けがありません。

リスト 6. 渡された関数を無視する関数を持つデコレーター
>>> def spamrun(fn):
...     def sayspam(*args):
...         print "spam, spam, spam"
...     return sayspam
...
>>> @spamrun
... def useful(a, b):
...     print a**2 + b**2
...
>>> useful(3,4)
spam, spam, spam

最後の例である、有効に振る舞うデコレーターは、修飾されていない関数の動作を何らかの方法で拡張または変更します。

リスト 7. 修飾されていない関数の振る舞いを変更するデコレーター
>>> def addspam(fn):
...     def new(*args):
...         print "spam, spam, spam"
...         return fn(*args)
...     return new
...
>>> @addspam
... def useful(a, b):
...     print a**2 + b**2
...
>>> useful(3,4)
spam, spam, spam
25

useful() がどれほど便利なのか、あるいは addspam() がそれほど賢い拡張なのかどうかについて、とやかく言うかもしれませんが、少なくともこのメカニズムは有益なデコレーターで一般的に見られるパターンに従っています。


高位の抽象化の紹介

私の経験からすると、メタクラスを使用する目的は大抵の場合、クラスがインスタンス化されるときに、そのクラスに含まれるメソッドを変更することです。デコレーターでは現在、クラスのインスタンス化自体を変更することはできませんが、クラスに属するメソッドを柔軟にすることはできます。この機能ではインスタンス化の最中にメソッドやクラス属性を動的に追加あるいは除去することは無理でも、実行時に環境の条件に応じてメソッドがその振る舞いを変えられるようにします。技術的に言えば、デコレーターが適用されるのは class ステートメントの実行時ですが、最上位レベルのクラスにとっては、「実行時」というより「コンパイル時」に近くなります。デコレーターの実行時の決定を調整するのは、クラス・ファクトリーを作成するくらいに単純です。以下の例を見てください。

リスト 8. 堅牢でありながら深くネストされたデコレーター
def arg_sayer(what):
    def what_sayer(meth):
        def new(self, *args, **kws):
            print what
            return meth(self, *args, **kws)
        return new
    return what_sayer

def FooMaker(word):
    class Foo(object):
        @arg_sayer(word)
        def say(self): pass
    return Foo()

foo1 = FooMaker('this')
foo2 = FooMaker('that')
print type(foo1),; foo1.say()  # prints: <class '__main__.Foo'> this
print type(foo2),; foo2.say()  # prints: <class '__main__.Foo'> that

上記の @arg_sayer() の例は、限定された結果を得るためにいろいろな回り道を辿っていますが、以下の点で意義があります。

  • Foo.say() メソッドは、インスタンスによって異なる振る舞いをします。この例では、結局異なるのはデータ値のみですが、データ値は他の手段でも簡単に変えられます。ただし原則的には、デコレーターは実行時の判断に基づいてメソッドを完全に書き換えることもできます。
  • この例では修飾されていない Foo.say() メソッドは単なるプレースホルダーで、全体的な振る舞いはデコレーターによって決定されます。ただし別のケースでは、デコレーターが、修飾されていないメソッドの振る舞いを何らかの新しい機能と組み合わせることもあります。
  • すでに取り上げましたが、Foo.say() の変更は FooMaker() クラス・ファクトリーを使用して、あくまでも実行時に決定されます。最上位レベルに定義されたクラスでデコレーターを使用するほうがおそらく一般的で、最上位レベルのクラスはコンパイル時に有効な条件にのみ依存します (このほうが適切な場合は珍しくありません)。
  • このデコレーターはパラメーター化されています。厳密に言うと、実は arg_sayer() 自体はデコレーターではありません。どちらかと言うと、arg_sayer() によって返される関数、つまり what_sayer() が、クロージャーを使ってデータをカプセル化するデコレーター関数となっています。パラメーター化デコレーターは一般的ですが、必要な関数が 3 レベルの深さまでネストされることになります。

メタクラス領域への進出

前のセクションで述べたように、デコレーターはメソッドを追加したり削除するのではなく、メソッドを変更するだけなので、メタクラス・フックに完全に置き換わることはありません。ただし、現実にはまったくそうとも限りません。Python の関数であるデコレーターは、他の Python コードで可能なありとあらゆることを実行できます。あるクラスの .__new__() メソッドを修飾すれば、それがプレースホルダーだとしても、クラスに属するメソッドを変更することが実は可能なのです。このパターンが使用されている例はまだ見かけていませんが、これはある程度明らかだと思います。ひょっとすると、_metaclass_ 割り当ての改善にもなるかもしれません。

リスト 9. メソッドを追加および除去するデコレーター
def flaz(self): return 'flaz'     # Silly utility method
def flam(self): return 'flam'     # Another silly method

def change_methods(new):
    "Warning: Only decorate the __new__() method with this decorator"
    if new.__name__ != '__new__':
        return new  # Return an unchanged method
    def __new__(cls, *args, **kws):
        cls.flaz = flaz
        cls.flam = flam
        if hasattr(cls, 'say'): del cls.say
        return super(cls.__class__, cls).__new__(cls, *args, **kws)
    return __new__

class Foo(object):
    @change_methods
    def __new__(): pass
    def say(self): print "Hi me:", self

foo = Foo()
print foo.flaz()  # prints: flaz
foo.say()         # AttributeError: 'Foo' object has no attribute 'say'

上記の change_methods() デコレーターの例では、いくつかの固定メソッドがほとんど意味もなく追加および除去されています。もっと現実的な例では、前のセクションで紹介したパターンが使用されることになります。例えば、パラメーター化デコレーターには追加または除去対象のメソッドを表すデータ構造を使用することができます。あるいは、データベース照会のような環境に備わった機能でメソッドの追加や除去を決定することもできます。属するメソッドに対するこのような操作を以前のように関数ファクトリー内にラップして、最終決定を実行時まで遅らせることも可能です。最後に挙げた 2 つの手法は、_metaclass_ 割り当てよりも融通が利くかもしれません。例えば、以下のように拡張 change_methods() を呼び出すことができます。

リスト 10. 拡張 change_methods()
class Foo(object):
    @change_methods(add=(foo, bar, baz), remove=(fliz, flam))
    def __new__(): pass

呼び出しモデルの変更

デコレーターについて取り上げられる最も典型的な例は、関数またはメソッドに基本ジョブの他に「プラス・アルファを行わせる」こととして説明されると思います。例えば、Python Cookbook Web サイト (「参考文献」にリンクを記載) のようなところでは、デコレーターがトレース、ロギング、メモリー保存/キャッシング、スレッドのロック、そして出力のリダイレクトなどの機能を追加している例が記載されている場合があります。このような変更に関連するのが、(その本質は多少異なりますが) 「変更前」と「変更後」のデコレーターです。変更前後の修飾の興味深い使用方法としては、関数の引数の型と関数からの戻り値の型のチェックが考えられます。おそらくこのような type_check() デコレーターは、型が期待通りではない場合、例外を発生させたり、あるいは是正処置を行うことになります。

変更前後のデコレーターとある意味同じ流れで私がたどり着いたのは、R プログラミング言語と NumPy の特性となっている「要素単位 (elementwise)」の関数アプリケーションという考えです。これらの言語では通常、数値関数は要素のシーケンスに含まれるそれぞれの要素だけでなく、個別の数値にも適用されます。

確かに、map() 関数、リストの内包、そして最新のジェネレーターの内包では要素単位のアプリケーションが可能ですが、それには R のような振る舞いを実現させるために多少の予備手段が必要となります。具体的には、map() が返すシーケンスのタイプが常にリストであること、そして map() をシーケンスではなく単一の要素に渡すと呼び出しが失敗するという振る舞いです。以下に例を示します。

リスト 11. 失敗する map() 呼び出し
>>> from math import sqrt
>>> map(sqrt, (4, 16, 25))
[2.0, 4.0, 5.0]
>>> map(sqrt, 144)
TypeError: argument 2 to map() must support iteration

通常の関数を「拡張」するデコレーターは、以下のように簡単に作成できます。

リスト 12. 要素単位関数への関数変換
displaycode">
        
def elementwise(fn):
    def newfn(arg):
        if hasattr(arg,'__getitem__'):  # is a Sequence
            return type(arg)(map(fn, arg))
        else:
            return fn(arg)
    return newfn

@elementwise
def compute(x):
    return x**3 - 1

print compute(5)        # prints: 124
print compute([1,2,3])  # prints: [0, 7, 26]
print compute((1,2,3))  # prints: (0, 7, 26)

単に異なる戻りの型に組み込まれる compute() 関数を作成するのは、もちろん難しいことではありません。デコレーターに必要なのは結局のところ数行だけです。その一方、アスペクト指向プログラミングの支持という点に関して言えば、この例を使えば、異なるレベルで動作するコンサーンを分離することができます。そのため、多種多様な数値演算関数を作成し、そのそれぞれを、引数の型のテストや戻り値の型強制についての詳細を考えずに要素単位の呼び出しモデルに変換できます。

elementwise() デコレーターは、個別のものを対象に動作する関数や、シーケンスを対象に (シーケンス・タイプを維持しながら) 動作する関数にも同じく有効に機能します。演習として、上記の修飾呼び出しがイテレーターも許容して返すようにする方法を見つけてみてください (ヒント: 完了した要素単位の演算を繰り返し処理すれば簡単です。唯一渡されるのがイテレーター・オブジェクトだけの場合、繰り返し処理を使わないとわかりにくくなります)。

優れたデコレーターのほとんどは、このパラダイムを大いに活用して直交する対象物を組み合わせています。従来のオブジェクト指向プログラミングでは、複数の継承を許可する Python のような言語では特に、継承階層によってコンサーンをモジュール化しようと試みます。ただし、単にある祖先からあるメソッドを取得し、別の祖先からはそれとは別のメソッドを取得する場合、アスペクト指向の考え方より一層コンサーンが分離される概念が必要となります。ジェネレーターを最大限に活用するには、ミックス・アンド・マッチング・メソッドとは多少異なる課題について考えなければなりません。例えば、それぞれのメソッドがメソッド自体の「中心」にはないコンサーンに応じて動作を変えるようにするなどです。


デコレーターの修飾

この記事を終える前に紹介しておきたいものがあります。それは、時々私と一緒に記事を書いている Michele Simionato が作成した decorator という実に素晴らしい Python モジュールです。このモジュールは、デコレーターの開発を一層優れたものにします。ある種の再帰的簡潔さを備えた decorator モジュールの主要コンポーネントは、decorator() というデコレーターです。@decorator で修飾した場合と修飾しない場合とでは、修飾した場合のほうが関数を簡単に作成できます (関連資料については、「参考文献」を参照)。

このモジュールに関しては Michele 自身が非常に優れた資料を作成しているので、ここで説明を繰り返すことはしませんが、decorator モジュールによって解決される基本的な問題を指摘したいと思います。decorator モジュールには主なメリットが 2 つあります。このモジュールでデコレーターを作成すると、必要なネスト・レベルを減らすことができます (ネストさせるよりフラットなほうがベターです)。さらに興味深い使用方法として、修飾された関数を実際にメタデータ内の修飾sれていないバージョンと一致させることも可能です (私の例では一致していませんでした)。例えば、上記で使ったやや間の抜けた「トレース」デコレーター、addspam() を思い出してください。

リスト 13. ネイティブ・デコレーターによるメタデータの損壊
>>> def useful(a, b): return a**2 + b**2
>>> useful.__name__
'useful'
>>> from inspect import getargspec
>>> getargspec(useful)
(['a', 'b'], None, None, None)
>>> @addspam
... def useful(a, b): return a**2 + b**2
>>> useful.__name__
'new'
>>> getargspec(useful)
([], 'args', None, None)

修飾された関数はその拡張ジョブを行いますが、よく調べてみると、まったく正しいわけではないことがわかります。それは特に、このような詳細を扱うコード分析ツールや IDE の場合はなおさらです。そこで、事態を改善するために以下のように decorator を使ってみます。

リスト 14. デコレーターの賢い使用方法
>>> from decorator import decorator
>>> @decorator
... def addspam(f, *args, **kws):
...     print "spam, spam, spam"
...     return f(*args, **kws)
>>> @addspam
... def useful(a, b): return a**2 + b**2
>>> useful.__name__
'useful'
>>> getargspec(useful)
(['a', 'b'], None, None, None)

上記の方が、最初にデコレーターを作成するにも、デコレーターの振る舞いを維持するメタデータでデコレーターを作成するにも良さそうです。もちろん、Michele がモジュールを開発するために使った呪文を端から端まで読んでいくと脳みそが溶けてしまいそうなため、Simionato 博士のような宇宙学者にそこのところは任せるとします。

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=251534
ArticleTitle=魅力的な Python: デコレーターで魔法を身近に
publish-date=12292006