[Python] decoratorモジュール

はじめに

この記事は、The decorator moduleの日本語訳です。TurboGearsの開発者はこのモジュールを参考にしたと、turbogears/decorator.pyに記述されています。

# Inspired by Michele Simionato's decorator library
# http://www.phyast.pitt.edu/~micheles/python/documentation.html

諸元

作者: Michele Simionato
E-mail: michele.simionato@gmail.com
バージョン: 2.0.1
ダウンロード元: http://www.phyast.pitt.edu/~micheles/python/decorator-2.0.1.zip
インストール方法: easy_install decorator
ライセンス: Python license

目次

  1. はじめに
  2. 定義
  3. 問題点
  4. 解決方法
  5. decoratorはデコレータ
  6. memoize
  7. locked
  8. delayed and threaded
  9. blocking
  10. redirecting_stdout
  11. tail_recursive
  12. 警告と制限
  13. LICENCE

はじめに

Python 2.4のデコレータは、シンタックス・シュガーの問題として、興味深い例です。原則として、それらを取り入れても何も変わりません。デコレータは、Pythonにまだない新しい機能を提供しないからです。実際には、私たちがPythonでプログラムを書く際に、デコレータは大きな変化をもたらします。私は、その変化は最良で、デコレータは素晴らしいアイデアだと確信します。なぜならば、

  1. デコレータは、定型的なコードを減らすのに役立ちます。
  2. デコレータは、関心事を分離 (separation of concerns) するのに役立ちます。
  3. デコレータによって、読みやすく、メンテナンスしやすくなります。
  4. デコレータは、大変明快です。

それにも関らず、現在、自分でデコレータを正しく書くのにはいくらかの経験が必要で、簡単ではありません。例えば、典型的なデコレータの実装にはネストした関数が関っており、私たちはみんなネストするよりフラットな方がいいことを知っています。

decoretorモジュールの狙いは、平均的なプログラマでもできるようにデコレータの使い方を簡単にすることであり、memoizeやtracing, redirecting_stdout, lockedなどといった役立つデコレータの例を示すことでデコレータの使用を一般的にすることです。

このモジュールの中心は、decoratorと呼ばれるデコレータ・ファクトリです。この文章で述べられるすべてのデコレータは、decoratorを使った単純なレシピとして作成されています。あなたはこれらのソースコードを_main.pyファイルで見つけるでしょう。_main.pyは、このドキュメントでdoctester(decoratorパッケージの中に入っています)を実行すると自動的に生成されます。

$ python doctester.py documentation.txt

同時に、doctesterはここにテストケースとして書かれているすべての例を実行します。

定義

技術的には、ひとつの引数をとって呼び出すことが可能ないかなるPythonオブジェクトもデコレータとして使用できます。しかしこの定義は、実際に使うにはいささか広義的です。一般的なデコレータのクラスをふたつのグループにわけると、もっと都合がいいです:

  1. シグネチャを保持するデコレータ、すなわち、入力としてひとつの関数をとり、同じシグネチャを持つ関数を出力として返す呼び出し可能なオブジェクト。
  2. シグネチャを変更するデコレータ、すなわち、入力として渡された関数のシグネチャを変更するか、呼び出し不可能なオブジェクトを返すデコレータ。

シグネチャを変更するデコレータには、独自の使い方があります: 例えば、組み込みクラスのstaticmethodとclassmethodはこのグループに入ります。なぜならば、それらは関数を引数にとり、関数でも呼び出し可能でもないディスクリプタ・オブジェクトを返します。

しかし、シグネチャを保持するデコレータはより一般的で、簡単です。とくに、シグネチャを保持する複数のデコレータが一緒に使用されます。一般的な他のデコレータでは不可能なのに(例えば、staticmethodとclassmethodを一緒に使っても意味はありません)。

シグネチャを保持するデコレータをスクラッチから書くのは簡単ではありません。とくに、いかなるシグネチャの関数も受け付ける適切なデコレータを定義するときにはそうです。簡単な例でこの問題を明らかにします。

問題点

あなたが関数をトレースしたいとしましょう: これはデコレータの典型的な使い方であり、あなたは多くの場所でこんなコードを見つけます:

#<_main.py>

try:
    from functools import update_wrapper
except ImportError: # using Python version < 2.5
    def decorator_trace(f):
        def newf(*args, **kw):
           print "calling %s with args %s, %s" % (f.__name__, args, kw)
           return f(*args, **kw)
        newf.__name__ = f.__name__
        newf.__dict__.update(f.__dict__)
        newf.__doc__ = f.__doc__
        newf.__module__ = f.__module__
        return newf
else: # using Python 2.5+
    def decorator_trace(f):
        def newf(*args, **kw):
            print "calling %s with args %s, %s" % (f.__name__, args, kw)
            return f(*args, **kw)
        return update_wrapper(newf, f)

#</_main.py>

デコレータが一般的なシグネチャの関数を受け付けるという意味では、この実装は動作します; 不運なことに、この実装はシグネチャを保持するデコレータを定義しません。なぜならば、一般的なdecorator_traceは元々の関数とは異なるシグネチャを持つ関数を返すからです。

例えば、この場合を見て下さい:

>>> @decorator_trace
... def f1(x):
...     pass

ここでは、元の関数はxという名前の引数をひとつ取りますが、デコレータをつけられた関数は任意の数の引数とキーワード引数を取ります。

>>> from inspect import getargspec
>>> print getargspec(f1)
([], 'args', 'kw', None)

これは、pydocといった内部を扱うツールが、f1のシグネチャについて間違った情報を与える、という意味です。これは大変よくありません: pydocはあなたにこの関数は一般的なシグネチャ*args, **kwを受け付けると教えるでしょうが、ふたつ以上の引数をこの間数に与えて呼び出そうとすると、エラーになります:

>>> f1(0, 1)
Traceback (most recent call last):
   ...
TypeError: f1() takes exactly 1 argument (2 given)

解決方法

解決方法は、一般的なデコレータのファクトリを提供し、それでシグネチャを保持するデコレータを作成する際の複雑なことを、アプリケーションプログラマから隠すことです。このデコレータ・ファクトリで、ネストした関数やクラスを使うことなく、デコレータを定義できます。例えば、decorator_traceは次のようにして定義できます:

まず、decoratorをインポートします。

>>> from decorator import decorator

それから、(f, *args, **kw)というシグネチャで、ヘルパー関数を定義します。このヘルパー関数は、元の関数fをargsとkwを使って呼び出し、トレースする機能を実装します。

#<_main.py>

def trace(f, *args, **kw):
    print "calling %s with args %s, %s" % (f.func_name, args, kw)
    return f(*args, **kw)

#</_main.py>

decoratorは、ヘルパー関数をシグネチャを保持するデコレータオブジェクトに変換できます。すなわち、ひとつの関数を引数にとり、元の関数と同じシグネチャを持つ、デコレータがついた関数を返す、呼び出し可能なオブジェクトになります。

>>> @decorator(trace)
... def f1(x):
...     pass

すぐにf1の動作を検証できます。

>>> f1(0)
calling f1 with args (0,), {}

そして、これは正しいシグネチャを持っています。

>>> print getargspec(f1)
(['x'], None, None, None)

同じdecoratorは、いかなるシグネチャの関数でも動作します。

>>> @decorator(trace)
... def f(x, y=1, z=2, *args, **kw):
...     pass

>>> f(0, 3)
calling f with args (0, 3, 2), {}

>>> print getargspec(f)
(['x', 'y', 'z'], 'args', 'kw', (1, 2))

変わったシグネチャを持つ関数でも、次のように動作します:

>>> @decorator(trace)
... def exotic_signature((x, y)=(1,2)): return x+y

>>> print getargspec(exotic_signature)
([['x', 'y']], None, None, ((1, 2),))
>>> exotic_signature()
calling exotic_signature with args ((1, 2),), {}
3

decoratorはデコレータ

デコレータ・ファクトリ自身は、classmethodやstaticmethodと同じように、シグネチャを変更するデコレータと考えることができます。しかし、classmethodとstaticmethodは呼び出し不可能な一般的なオブジェクトを返しますが、decoratorはシグネチャを保持するデコレータを返します。すなわち、引数がひとつの関数です。したがって、

>>> @decorator
... def tracing(f, *args, **kw):
...     print "calling %s with args %s, %s" % (f.func_name, args, kw)
...     return f(*args, **kw)

と書くこともできます。実際、このイディオムはtracingをデコレータとして再定義します。私たちは、シグネチャが変わったことを簡単に調べられます:

>>> print getargspec(tracing)
(['f'], None, None, None)

したがって、これからtracingはデコレータとして使うことができ、以下のコードは動作します:

>>> @tracing
... def func(): pass

>>> func()
calling func with args (), {}

ところで、lambda関数にもデコレータを使うことができます:

>>> tracing(lambda : None)()
calling <lambda> with args (), {}

この文章の残りで、decoratorの上に作成された便利なデコレータの例を述べます。

memoize

このデコレータは、memoizeパターンを実装します。つまり、これは関数の結果を辞書に保存しておき、次に同じ引数で関数が呼ばれたときにはキャッシュから結果を取り出し、再計算しません。

#<_main.py>

from decorator import *

def getattr_(obj, name, default_thunk):
    "Similar to .setdefault in dictionaries."
    try:
        return getattr(obj, name)
    except AttributeError:
        default = default_thunk()
        setattr(obj, name, default)
        return default

@decorator
def memoize(func, *args):
    dic = getattr_(func, "memoize_dic", dict)
    # memoize_dic is created at the first call
    if args in dic:
        return dic[args]
    else:
        result = func(*args)
        dic[args] = result
        return result

#</_main.py>

使い方は、次の通りです。

>>> @memoize
... def heavy_computation():
...     time.sleep(2)
...     return "done"

>>> print heavy_computation() # the first time it will take 2 seconds
done

>>> print heavy_computation() # the second time it will be instantaneous
done

練習として、デコレータ・ファクトリを使わないでmemoizeを適切に実装してみて下さい。

このmemoizeはキーワード引数を持たない関数にしか使えないことに注意して下さい。なぜならば、ミュータブル(変更可能)な引数に依存しているものを正しく保存しておくことは不可能だからです。この制限を緩和することは可能で、シグネチャにキーワード引数を含めるようにできます: しかし、もしキーワード引数が渡されたら、結果はキャッシュされないようにすべきです。例として、http://www.python.org/moin/PythonDecoratorLibraryを参照して下さい。

locked

マルチスレッドプログラミングにおいて、デコレータのいい使い方があります。例えば、lockedデコレータはロックを取得/解除する定型的なコードを取り除くことができます[1]。

#<_main.py>

import threading

@decorator
def locked(func, *args, **kw):
    lock = getattr_(func, "lock", threading.Lock)
    lock.acquire()
    try:
        result = func(*args, **kw)
    finally:
        lock.release()
    return result

#</_main.py>

[1] Python 2.5では、ロックを取り扱うwith文が用意されています: http://docs.python.org/dev/lib/with-locks.html

使い方の例として、一度に一人のユーザからしかアクセスできないような外部のリソース(例えばプリンタ)にデータを書き込むとします。この場合、書き込む関数へのアクセスはロックされる必要があります:

#<_main.py>

import time

datalist = [] # for simplicity the written data are stored into a list.

@locked
def write(data):
    "Writing to a sigle-access resource"
    time.sleep(1)
    datalist.append(data)

#</_main.py>

書き込む関数がロックされるので、いかなる場合においても、書き込む人は最大でも一人であることが保証されます。データを書き込み、表示するマルチスレッドプログラミングの例が、次の節にあります。

delayedとthreaded

しばしば、デコレータをひとまとめにしたいことがあります。すなわち、ひとつ以上のパラメータに依存するデコレータです。

ここで、delayedデコレータという、ひとつのパラメータをとるデコレータの例を考えます。このデコレータはひとつのプロシージャをとり、それを遅延するプロシージャに変換します。この場合、遅延する時間がパラメータです。

遅延するプロシージャは、呼び出されたら、指定された時間だけ後で別のスレッドによって実行されます。実装は難しくはありません:

#<_main.py>

def delayed(nsec):
    def call(proc, *args, **kw):
        thread = threading.Timer(nsec, proc, args, kw)
        thread.start()
        return thread
    return decorator(call)

#</_main.py>

decoratorがなくても、ネストする必要があることに注意して下さい。

プロシージャ、すなわち関数に使われたdelayedデコレータは、Noneを返します。なぜならば、この実装では元の関数の戻り値は捨てられるからです。デコレートされた関数は現在実行中のスレッドを返し、これを保存して、例えばスレッドが.isAlive()であるか後で検査できます。

遅延されたプロシージャは、多くの場合に有用です。例えば、私はこのパターンを、ウェブサーバがスタートした後にウェブブラウザを立ち上げるときに使っています。そのコードは以下のようになります。

>>> @delayed(2)
... def start_browser():
...     "code to open an external browser window here"

>>> #start_browser() # will open the browser in 2 seconds
>>> #server.serve_forever() # enter the server mainloop

遅延がない特別な場合は、別の名前をつけるべきであるくらい重要です:

#<_main.py>

threaded = delayed(0) # 遅延がないデコレータ

#</_main.py>

threadedデコレータがつけられたプロシージャは、呼び出されたときに別のスレッドで実行されます。前述した書き込むルーチンは、以下のようになります:

>>> @threaded
... def writedata(data):
...     write(data)

writedateを呼び出すごとに新しい書き込み用のスレッドが生成されますが、書き込みはロックされないので、同期はしません。

>>> writedata("data1")
<_Timer(Thread-1, started)>

>>> time.sleep(.1) # 少し待って、data2がdata1の後に書き込まれることを確実にします。

>>> writedata("data2")
<_Timer(Thread-2, started)>

>>> time.sleep(2) # 書き込みが完了するのを待ちます。

>>> print datalist
['data1', 'data2']

** blocking

ときには標準入力のようなブロッキングするリソースを扱う必要があり、ときにはブロックするより"busy"メッセージを返した方がいいこともあります。この挙動は、適切なデコレータを実装することで可能です。

>|python|
#<_main.py>

def blocking(not_avail="Not Available"):
    def call(f, *args, **kw):
        if not hasattr(f, "thread"): # no thread running
            def set_result(): f.result = f(*args, **kw)
            f.thread = threading.Thread(None, set_result)
            f.thread.start()
            return not_avail
        elif f.thread.isAlive():
            return not_avail
        else: # the thread is ended, return the stored result
            del f.thread
            return f.result
    return decorator(call)

#</_main.py>

blockingでデコレートされた関数は、リソースが利用できない場合はビジーメッセージを返し、リソースが利用できる場合は実行した結果を返します。例えば:

>>> @blocking("Please wait ...")
... def read_data():
...     time.sleep(3) # ブロックするリソースをシミュレートする。
...     return "some data"

>>> print read_data() # データはまだ利用できない。
Please wait ...

>>> time.sleep(1)
>>> print read_data() # データはまだ利用できない。
Please wait ...

>>> time.sleep(1)
>>> print read_data() # データはまだ利用できない。
Please wait ...

>>> time.sleep(1.1) # 3.1秒後、データが利用可能になる。
>>> print read_data()
some data

redirecting_stdout

デコレータは、try .. finallyブロックで構成される定型的なコードを排除するのに役立ちます。私たちはlockedの場合を見ました; 別の例を以下に記します:

#<_main.py>

import sys

def redirecting_stdout(new_stdout):
    def call(func, *args, **kw):
        save_stdout = sys.stdout
        sys.stdout = new_stdout
        try:
            result = func(*args, **kw)
        finally:
            sys.stdout = save_stdout
        return result
    return decorator(call)

#</_main.py>

使い方は、以下の通りです:

>>> from StringIO import StringIO

>>> out = StringIO()

>>> @redirecting_stdout(out)
... def helloworld():
...     print "hello, world!"

>>> helloworld()

>>> out.getvalue()
'hello, world!\n'

同じテクニックがデータベースのトランザクションに関わる定型的なコードの排除に利用できます。あなたはアイデアを思いついたと思うので、トランザクションの例は読者の練習のために残しておきます。もちろんPython 2.5では、これらの場合はwith文でもできます。

tail_recursive

ネット上で、自分のコードに加えたいと思うようなデコレータを見つけることもあるでしょう。しかし、クックブックにも載っています。http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496691.

#<_main.py>

from decorator import update_wrapper

class TailRecursive(object):
    """
    tail_recursiveデコレータは、Kay Schluehrのレシピに基づいています。
    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496691
    """
    CONTINUE = object() # sentinel

    def __init__(self, func):
        self.func = func
        self.firstcall = True

    def __call__(self, *args, **kwd):
        try:
            if self.firstcall: # start looping
                self.firstcall = False
                while True:
                    result = self.func(*args, **kwd)
                    if result is self.CONTINUE: # update arguments
                        args, kwd = self.argskwd
                    else: # last call
                        break
            else: # return the arguments of the tail call
                self.argskwd = args, kwd
                return self.CONTINUE
        except: # reset and re-raise
            self.firstcall = True
            raise
        else: # reset and exit
            self.firstcall = True
            return result

def tail_recursive(func):
    "TailRecursiveを、シグネチャを保持するデコレータに変換する"
    return update_wrapper(TailRecursive(func), func, create=True)

#</_main.py>

古き良き階乗にこのデコレータを適用する場合は、以下のようにします:

#<_main.py>

@tail_recursive
def factorial(n, acc=1):
    "古き良き階乗"
    if n == 0: return acc
    return factorial(n-1, n*acc)

#</_main.py>
>>> print factorial(4)
24

このデコレータはとても刺激的で、あなたにひらめきを与えたことでしょう;)。いまは再帰する回数に制限はなく、あなたはfactorial(1001)やもっと大きな数値を、スタックフレームを使いきることなく簡単に計算できることに注意して下さい。また、このデコレータは末尾再帰ではない関数には適用できないことにも注意して下さい。例えば、

def fact(n): # これは末尾再帰ではない。
    if n == 0: return 1
    return n * fact(n-1)

(再帰することなく値を返すか、再帰して得られた値を直接返す関数が、末尾再帰です)。

警告と制限

デコレータはパフォーマンスの上では不利であることに注意して下さい。以下は、悪い場合の例です:

$ cat performance.sh
python -m timeit -s "
from decorator import decorator

@decorator
def do_nothing(func, *args, **kw):
    return func(*args, **kw)

@do_nothing
def f():
    pass
" "f()"

python -m timeit -s "
def f():
    pass
" "f()"

私のLinuxでは、普通の関数の代わりにdo_nothingデコレータを使った方は4倍以上遅いです:

$ bash performance.sh
1000000 loops, best of 3: 1.68 usec per loop
1000000 loops, best of 3: 0.397 usec per loop

実際に使用される関数は、ここでのfよりももっと役に立つことをおそらくするでしょうから、実際に使用する上でのパフォーマンス上の劣化は完全に無視できます。いつものように、あなたの使い方で劣化があるかどうか知る唯一の方法は、それを計測することです。

デコレータはトレースバックを長くし、より理解を難しくすることを覚えておいて下さい。次の例を見てください:

>>> @tracing
... def f():
...     1/0

f()を呼び出すとZeroDivisionErrorが発生しますが、この関数はでこれーとされているので、トレースバックは長くなります:

>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
    f()
  File "<string>", line 2, in f
  File "<stdin>", line 4, in tracing
    return f(*args, **kw)
  File "<stdin>", line 3, in f
    1/0
ZeroDivisionError: integer division or modulo by zero

ここで、f(*args, **kw)を呼び出すtracingデコレータの内部の呼び出しと、File "", line 2, in fという参照を見ることができます。この参照は、decoratorモジュールがデコレートされた関数を生成するのにevalを使っているという事実によります。ここで、evalはパフォーマンスの劣化には関与しないことを述べておきます。なぜならば、evalは関数をデコレートするときに一回だけ呼び出され、デコレートされた関数が呼び出されるごとに呼び出されるわけではないからです。

現在、evalを回避する方法はありません。美しく解決しようと思ったら、CPythonの関数の実装を変更し、関数のシグネチャを直接変更することを可能にするフックを追加する必要があります。これは将来のPythonのバージョンでなされるかもしれません。そのとき、このモジュールは不要になります。

デバッグのため、decoratorモジュールにはgetinfoユーティリティ関数があり、それを使うと関数の情報を持つ辞書が得られるということを、知っておくといいでしょう。例えば、fractorial関数では、次が得られます。

>>> d = getinfo(factorial)
>>> d['name']
'factorial'
>>> d['argnames']
['n', 'acc']
>>> d['signature']
'n, acc'
>>> d['defaults']
(1,)
>>> d['doc']
'The good old factorial'

現在の実装では、decoratorによって生成されたデコレータは、ユーザ定義のPython関数かメソッドにしか使えず、一般的な呼び出し可能なオブジェクトや組み込み関数には使えません。これは標準ライブラリのinspectモジュールの制限によるためです。また、引数の名前にも制限があります: _call_または_func_引数を呼び出そうとすると、AssertionErrorが発生します:

>>> @tracing
... def f(_func_): print f
...
Traceback (most recent call last):
  ...
AssertionError: You cannot use _call_ or _func_ as argument names!

(この2つの予約語が存在するのは、実装の細かいことになります)。

さらに、デコレートされた関数は、元の関数の属性のコピーを持ちます。

>>> def f(): pass # 元の関数
>>> f.attr1 = "something" # 属性を設定する。
>>> f.attr2 = "something else" # さらに属性を設定する。

>>> traced_f = tracing(f) # デコレートされた関数

>>> traced_f.attr1
'something'
>>> traced_f.attr2 = "something different" # 属性を設定する。
>>> f.attr2 # 元の属性は変更されない。
'something else'

以上です。楽しんでください!

ライセンス

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in bytecode form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.

もしあなたがこのソフトウェアで幸せになったら、私にそれを教えて、私のエゴを満足させて下さい。一方、あなたがこのソフトウェアを使って不幸になったら、パッチを送って下さい!