5/08/2011

[Python][翻訳]関数デコレータ

Pythonの関数デコレータについて調べていたところ、
Dive Into Python 3 Appendix.C 「ここからどこへ進むのか」にて、Ariel Ortizによる 関数デコレータ、続関数デコレータ が紹介されていた。
早速見てみると、分かり易くて初学者には良いなと思った。
どうせ、記憶力がない私のこと、何度も忘れて見に行くことを考えると、
一度、勉強がてら、翻訳しておこかなぁ?と思い、
作者のAriel Ortizに翻訳とブログへの掲載・コメント
許可を求めたところ「OK」とのお返事を頂きました。

Ariel Ortizのブログページ ProgrammingBits

英語もPythonもアレな私が翻訳しているので、少しでもオカシイところがあれば、
それは私の責です。原文をあたって下さいませ。突っ込みお願いします。


関数デコレーター

プログラミング言語Pythonは、デコレーターと呼ばれる興味深い
シンクタックス上の機能を備えている。

どうやって、そしてどうしてPythonのでデコレーター
を使うのか?さあ、例題を1個使って説明しよう。

NOTE:ここで例示する、すべてのPythonコードはPython3に基づいている。
ソースコードの全文は以下。
http://programmingbits.pythonblogs.com/gallery/27/function_decorators.py

再帰関数を使って、フィボナッチ数を計算したいとする。
次の関数定義で、うまくいく。(1)

def fib(n):
    if n in (0, 1):
        return n
    else:
        return fib(n - 1) + fib(n - 2)

これで異なる入力値で関数をテストすることができる。

>>> fib(0)
0
>>> fib(1)
1
>>> fib(4)
3
>>> fib(10)
55
>>> fib(30)
832040
>>> fib(35)
9227465

あなたが、正確にこれらサンプルをPython shellにタイプしたならば
入力値が大きいほど、結果を得るのに時間がかかることに気がついただろう。

高速に動かすために、memoizationと呼ばれるテクニックを使うことができる。
memoizeされた関数は、特定の入力値に該当する結果を、キャッシュに格納する。
呼び出しの後、前回の入力値に対する計算結果として、結果をリターンすると共に、
キャッシュに格納する。すなわち、再び計算するのを避けるのだ。

このことは、特定のパラメータでの関数コールにかかる主要コストは、
次に同じパラメータでよばれた関数に最初にかかるコストを、
肩代わりしていることになる。

fib関数を直接変更するかわりに、再利用可能なmemoizing関数を書くほうがよいだろう。

def memoize(f):
    cache = {}
    def helper(x):
        if x not in cache:            
            cache[x] = f(x)
        return cache[x]
    return helper

memoize関数は別のhelper関数をラップしている。
そしてパラメータ f として受け取った関数に追加機能をする。

helper関数は実際にレキシャルクロージャーだ。レキシャルクロージャーとは、関数自身が作成された時の、自身のスコープ内にある変数を記憶している関数オブジェクトである。

このケースでいうと、helperは変数 fとcache(関数オブジェクトと辞書をそれぞれ保持する)を覚えていて最後の行では、memoize関数の呼び出し元にhelper lexicalクロージャーを返している。


以下のコードをタイプしてみると
fibをコールしたときに、すぐにスピードがアップしていることに気がつくだろう。

すごく初期のコール時でさえも、スピードアップを認めることができる。
何故なら、直ぐにfibの内部に再帰呼び出しを積み上げるからだ。

>>> fib = memoize(fib)
>>> fib(50)
12586269025

最初の行はfibをmemoizeでデコレートしていると読むことができる。
何故なら、これはPythonにおける、共通のプログラミングのイディオムのようなもので、
それを単純に利用するために、特別なデコレータシンクタックスがある。
このシンクタックスはJavaのアノテーションにインスパイアされたもので、
デコレートされる関数の定義の前に、@につづけてデコレータ関数の名前を記述する。

別の言い方をすると、このPythonのシンクタックスは

@some_decorator
def some_function():
    # function body.

以下と同じ意味である。

def some_function():
    # function body...
some_function = some_decorator(some_function)

ほとんどの場合は
@ノーテーションの方が読みやすくエラーになりにくいと同意すべきだろう。

フィボナッチ、memoization関数の例で、Pythonデコレーターを
利用するために、次のように書き直さなくてはならないだろう。

def memoize(f):
    # 前出のコード...
 
@memoize 
def fib(n):
    # 前出のコード

memoize関数は典型的な関数デコレータが持っているべき一般的な構造をみせてくれる。
一つの関数 f をインプットパラメータとして受け取り、関数 f に追加の責務(出来ること)を追加する
別の関数を戻り値とするのだ。

Pythonデコレータの別の面白い点としては、2つ以上のそれを連結することができるというものだ。
それでは、 デコートされた関数が呼ばれる前と、戻り値を返す前にメッセージを出力するような
トレース・デコレータを加えるためにサンプルを拡張してみよう。

def memoize(f):
    # 前出のコード...
 
def trace(f):
    def helper(x):
        call_str = "{0}({1})".format(f.__name__, x)
        print("Calling {0} ...".format(call_str))
        result = f(x)
        print("... returning from {0} = {1}".format(
              call_str, result))
        return result
    return helper
 
@memoize
@trace
def fib(n):
    # 前出のコード...

@memoizeと@traceはfibの直前に定義している。
さっそく、fibを実行してみると最初にmemoizeデコレータが呼ばれる。それからtraceデコレータが呼ばれ
最後に、オリジナルのfibコードが呼ばれる。例を確認してみよう。

>>> fib(5)
Calling fib(5) ...
Calling fib(4) ...
Calling fib(3) ...
Calling fib(2) ...
Calling fib(1) ...
... returning from fib(1) = 1
Calling fib(0) ...
... returning from fib(0) = 0
... returning from fib(2) = 1
... returning from fib(3) = 2
... returning from fib(4) = 3
... returning from fib(5) = 5
5

Pythonは関数デコレータとして使用されることを意図した、3種類のビルトイン関数を持っている。

  • classmethod: デコレータメソッドが、クラスメソッドであることを指す為に使用される。SmalltalkやRubyのそれに似ている。
  • staticmethod: デコレータメソッドが、スタティックメソッドであることを指す為に使用される。C++、C#やJAVAのそれに似ている。
  • property: オブジェクトのプロパティを取得、設定、削除するために使用される。

Python3.0は関数をデコレータするだけでなく、クラスをデコレートすることも許可することは、注意しておく価値がある。
うまくいけば、こんどのポストでこれらの特定種類のデコレータについて書くために時間を取るつもりだ。。

Notes
(1) 再帰を用いたフィボナッチ数列の実装はとても効率が悪いのは、知っている。
だけど、それが私の意図だ。
遅さを体感できるよう、私は単純なアルゴリズムがほしかった。



以上です。アホな私にも理解できたような気がする。分かりやすいです。
続・関数デコレータも読も。

6/11 修正

0 件のコメント: