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)

  1. def fib(n):  
  2.     if n in (01):  
  3.         return n  
  4.     else:  
  5.         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関数を書くほうがよいだろう。

  1. def memoize(f):  
  2.     cache = {}  
  3.     def helper(x):  
  4.         if x not in cache:              
  5.             cache[x] = f(x)  
  6.         return cache[x]  
  7.     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のシンクタックスは

  1. @some_decorator  
  2. def some_function():  
  3.     # function body.  

以下と同じ意味である。

  1. def some_function():  
  2.     # function body...  
  3. some_function = some_decorator(some_function)  

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

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

  1. def memoize(f):  
  2.     # 前出のコード...  
  3.   
  4. @memoize   
  5. def fib(n):  
  6.     # 前出のコード  

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

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

  1. def memoize(f):  
  2.     # 前出のコード...  
  3.    
  4. def trace(f):  
  5.     def helper(x):  
  6.         call_str = "{0}({1})".format(f.__name__, x)  
  7.         print("Calling {0} ...".format(call_str))  
  8.         result = f(x)  
  9.         print("... returning from {0} = {1}".format(  
  10.               call_str, result))  
  11.         return result  
  12.     return helper  
  13.   
  14. @memoize  
  15. @trace  
  16. def fib(n):  
  17.     # 前出のコード...  

@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 件のコメント: