Pythonのデコレーターの使い方

数カ月前にPythonのデコレーターを勉強した時はよく分かんなかったんですが、今日勉強してみたらなんか分かった気がするのでめも。間違ってたらごめんなさい。
以下のサイトを参考にしてます。


Python decorator
日本語で分かりやすいです。
PEP 318 -- Decorators for Functions and Methods | Python.org
例はここのを使用してます。

デコレーターって何?

Pythonでクラスメソッドを作成するとき、以前*1は、

class x():
  def foo(cls):
    pass
  foo = classmethod(foo)

と、する必要がありました*2。これじゃ面倒くさいよねってことで考えられたのがデコレーターです。デコレーターを使うと上のクラスは次のように書き換えることができます。

class x():
  @classmethod
  def foo(cls):
     pass

このように、ある関数(ここではfoo)を変形させるのに使用するのがデコレーターです。ここでは組み込み関数のclassmethod()を使いましたが、自分で定義した関数を使うこともできます。

デコレーターの構文

デコレーターは次のように使います。

@dec1
def func(arg1,arg2,...):
  pass

これは次の文と等価です。

def func(arg1,arg2,...):
  pass
func = dec1(func)


例1: 実行時に登録される関数を定義 (この例では関数自体に変更はされません)

def onexit(f):
  import atexit
  atexit.register(f)
  return f

@onexit
def func():
  ...

(注:atexit.register()で登録した関数はインタプリタが終了するときに自動的に実行されます。)
あくまでもこれは例なので実用的ではありません。func = onexit(func)を実行するので、デコレーターは必ず戻り値を返す必要があります。


デコレーターは複数適用させることができます。

@dec2
@dec1
def func(arg1,arg2,...):
  pass

これは、 func = dec2(dec1(func)) としているのと一緒です。合成関数 (g o f)(x) が g(f(x)) になるような感じです。
また、デコレーター自体に引数を与えることができます。

@dec(argA,argB,...)
def func(arg1,arg2,...):
  pass

これは次の文と等価です。

func = dec(argA,argB,...)(func)


例2: 関数に属性を追加

def attrs(**kwds):
  def decorate(f):
    for k in kwds:
      setattr(f,k,kwds[k])
    return f
  return decorate

@attrs(versionadded="2.2",
       author="Guido van Rossum")
def mymethod(f):
  ...

関数が入れ子になっていてややこしいですが、次のように考えるといいんだと思います。
1. @attrs(versionadded="2.2",author="Guido van Rossum") なので、 attrs(versionadded="2.2",author="Guido van Rossum")が実行される。この戻り値として、kwdsが与えた引数に置きかえられたdecorate関数が返る。
2. mymethod = (attrs(versionadd...)の戻り値の関数)(mymethod)が実行される。つまり、結果としてはmymethod = decorate(mymethod)が実行される。
3. decorate()内でmymehodにsetattr()によって属性が追加される。
実際の内容はdis/inspect モジュールを使った Python のハッキングが参考になると思います。


例3: デコレータによって引数と戻り値の型を強制

def accepts(*types):
  def check_accepts(f):
    def new_f(*args, **kwds):
      for (a, t) in zip(args, types):
        assert isinstance(a, t), \
                "arg %r does not match %s" % (a,t)
      return f(*args, **kwds)
    new_f.func_name = f.func_name
    return new_f
  return check_accepts

def returns(rtype):
  def check_returns(f):
    def new_f(*args, **kwds):
      result = f(*args, **kwds)
      assert isinstance(result, rtype), \
            "return value %r does not match %s" % (result,rtype)
      return result
    new_f.func_name = f.func_name
    return new_f
  return check_returns

@accepts(int, int)
@returns(int)
def func(arg1, arg2):
  return arg1 * arg2


print func(1,2)
print func(1.0,2)   # 引数エラー

この例では、check_returns(f)内でさらに関数を定義して、それを返すことで関数f()を根本的に書き換えています。まずはじめに@returnsによってfuncが書き換えられ、その書き換えられた関数に対して@acceptが適用されます。実際にfunc()を実行するときにはまず引数がチェックされ、その後元のfunc()を呼びだし、最後に戻り値をチェックしています。


これで一通りデコレーターについての説明は終わりです。まぁデコレーターを使うのは簡単ですが、作るのは慣れないとなかなか大変だと思います。PythonDecoratorLibrary - Python Wikiというページにデコレーターのサンプルがたくさん用意されてるので、これを参考にするといいと思います。
上のサイトからいくつか紹介します。


例4: メモ化 (3.Memoize)

class memoized(object):
  def __init__(self,func):
    self.func = func
    self.cache = {}
  def __call__(self,*args):
    try:
      return self.cache[args]
    except KeyError:  # キャッシュされていない場合
      value = self.func(*args)
      self.cache[args] = value
      return value

@memoized
def fibonacci(n):
  if n in (0,1):
    return n
  return fibonacci(n-1) + fibonacci(n-2)

ここではデコレータとしてクラスを使っています。結果としてfibonacciはmemoizedのインスタンスになります(fibonacci = memoized(fibonacci))。
同じことを関数でやることももちろんできます。

def memoize(func):
  cache = {}
  def memoized_function(*args):
    try:
      return self.cache[args]
    except KeyError:
      value = func(*args)
      cache[args] = value
      return value
  return memoized_function

@memoize
def fibonacci(n):
  if n in (0,1):
    return n
  return fibonacci(n-1) + fibonacci(n-2)

まぁ自分が分かりやすい方を使えばいいと思います。


例5: 関数が呼ばれた回数をカウントする (8.Counting function calls

class countcalls(object)
  __instances = {}

  def __init__(self,f):
    self.__f = f
    self.__numcalls = 0
    countcalls.__instances[f] = self

  def __call__(self,*args,**kwargs):
    self.__numcalls += 1
    return self.__f(*args,**kwargs)

  @staticmethod
  def count(f):
    return  countcalls.__instances[f].__numcalls

  @staticmethod
  def counts():
    return dict([(f,countcalls.conut(f)) for f in countcalls.__instances])

こんな感じで使えます。

@countcalls
def f():
  print "f"

@countcalls
def g():
  print "g"

f()
f()
f()
g()
g()
print countcalls.count("f")   # 3
print countcalls.counts()     # {'g': 2, 'f': 3}


他にも沢山ありますがとりあえず今日はここまでで^^;。

*1:Python2.4a2より前?

*2:余談ですが、Pythonのclassmethod()は他のC++Javaにおける静的メソッドとは異なりますhttp://jutememo.blogspot.com/2008/09/python-classmethod-staticmethod.html