高階関数としてのデコレータに関する考察

デコレータはPython2.4で追加された構文で、関数やメソッドのラッピングを行ないます。

@deco1
def foo()
  pass

@deco2(args)
def bar()
  pass

このプログラムは以下のプログラムと等価です。

def foo()
  pass
foo = deco1(foo)

def bar()
  pass
bar = deco2(args)(bar)

この構文は静的メソッドやクラスメソッドの定義を分かりやすく書きたいという動機から追加されました。
静的メソッドやクラスメソッドの定義は、デコレータ導入以前では以下のように書かれていました。

class C(object):
  def foo(cls, x):
    pass
  foo = classmethod(foo)

  def bar(y):
    pass
  bar = staticmethod(bar)

デコレータ構文を使用するとこれを簡潔に書くことができます。

class C(object):
  @classmethod
  def foo(cls, x):
    pass

  @staticmethod
  def bar(y):
    pass

他にも、デコレータ構文を使用すると、計算結果をキャッシュしたり実行時間の計測などの機能を簡単に追加することができます。

デコレータ構文の目的は関数やメソッドのラッピングであるため、デコレータ関数の戻り値の関数であることが期待されます。
しかし、デコレータ構文が単なるシンタックスシュガーであることを考えると、関数やメソッドの定義以外にも利用できそうです。

デコレータ関数が関数を1つ受け取る関数であることに着目すると、例えば、ファイルを開いて何か処理をしてファイルを閉じるという処理を、デコレータ構文を使って以下のように書くことができます。

def call_with_open_file(filename, *args):
  def _(fun):
    io = open(filename, *args)
    try:
      return fun(io)
    finally:
      io.close()
  return _

@call_with_open_file("/tmp/foo.txt", "w")
def _(io):
  for i in xrange(10):
    io.write("%d\n" % i)

@call_with_open_file("/tmp/foo.txt", "r")
def result(io):
  return io.read()
print result # /tmp/foo.txt の中身が表示される

この例のような単なるファイルのクローズだけなら、Python2.5から導入されたwithステートメントで実現することができます。
しかし、withステートメントがwith文に渡されたプログラムをデータとして扱えないのに対し、この方法ではデータ(関数オブジェクト)として扱うことができるというメリットがあります。また、with文には戻り値が存在しないのに対し、この構文を使うと、デコレータに渡した関数と同じ名前の変数にデコレータ関数の計算結果が保存されます(上記プログラムのresult変数)。

結論としては、デコレータ構文は、Rubyで言うブロック付きメソッドのように、関数引数が1つだけの高階関数のための構文と考えることができます。
そのため、構文が気持ち悪いという問題を除けば、色々な応用ができそうです。

以下の例は、Pythonのマルチスレッドプログラムをデコレータを使って実現した例です。

import threading
from time import sleep

def new_thread(*args, **kwargs):
  def _(fun):
    class _NewThread(threading.Thread):
      def run(self):
        return fun(*args, **kwargs)
    return _NewThread()
  return _

try:
  @new_thread()
  def thread_a():
    for i in xrange(10):
      print "a%d" % i
      sleep(0.01)
    return
  @new_thread()
  def thread_b():
    for i in xrange(10):
      print "b%d" % i
      sleep(0.01)
  thread_a.start()
  thread_b.start()
finally:
  thread_a.join()
  thread_b.join()