デコレータについて(デコレータを引数にしてデコレータを返す関数の作成)

デコレータを引数に、デコレータを返す関数を作って無かったので作ってみる。
今回は、関数用のデコレータをメソッド用のデコレータに変換する関数を作ってみる。

デコレータ

デコレータは以下のようなコードの構文糖衣。

@deco
def f(n):
   return n

def g(n):
   return n
g = deco(g)

## 関数が無名なら以下でも同じ。
h = deco(lambda n : n)

デコレータの種類。恣意的なので分類はてきとー。

  • 対象
    • クラス
    • 関数
    • メソッド
  • 作成する方法
    • 関数
    • クラス
      • callable object(__call__)
      • class method (static method)

と色々ある。だいたいのものは以前作ってみたことがある。
でも、そういえば、「デコレータを引数として取って、デコレータを返す関数」を作ったことが無かった。

なので作ってみることにする。

デコレータを引数として取って、デコレータを返す関数の作成

例. 関数用のデコレータをメソッド用のデコレータにする。

関数用デコレータのデコレータを直接メソッドに利用できない。

関数へのデコレータincを作成。デコレータを掛けた関数が、渡された引数に+1した値を利用するように変更するデコレータ。

def inc(f):
    return lambda n: f(n+1)

sq = inc(lambda x : x * x) #これはデコレータを掛けたのと同じこと

print sq(10) # 121

作ったincをメソッドにも適用しようとすると失敗する。まあ、当然。引数違うので。

class A(object):
    @inc
    def double(self, n):
        return n * 2

A().double(10)
# TypeError: <lambda>() takes exactly 1 argument (2 given)

上手く動作させるには、引数を合わせて、メソッド用にデコレータを作ってあげる必要がある。

def method_inc(method):
    return lambda self, n: method(self, n+1)

class A(object):
    @method_inc
    def double(self, n):
        return n * 2

print A().double(10) # 22

実際のところ、メソッドに対応させると言っても、修正は単にselfを加えるだけ。
関数呼び出し、メソッド呼び出しをまとめると以下のような感じ。

関数 lambda *args, **kw
インスタンスメソッド lambda self, *args, **kw

*args,**kwは引数全部ということ。selfはインスタンスメソッドを想定しているから。
もちろん、ひとつ引数が増えるのはクラスメソッドについても同様。(lambda cls, *args, **kwになる)

関数用のデコレータをメソッド用のデコレータにする。

いくら簡単と言っても、

  • 関数用のデコレータ
  • メソッド用のデコレータ

をいちいち2つ作らなければいけないのは面倒だし煩雑。

デコレータを変換する関数があれば良さそう。

具体的には、関数用のデコレータを引数として取って、メソッド用のデコレータを返す関数coerce_methodを作りたい。
実際に利用するときにはこんな感じで使うことを想定している。

class A(object):
    @coerce_method(inc)
    def double(self, n):
        return n * 2

実際に作ってみる。
初めはデコレータを取る、そのデコレータは関数で、これを変換してメソッド用に。

def coerce_method(fn_deco): 
    """fn_decoは関数デコレータ これをメソッドデコレータにして返す
    """
    def method_deco(method):
        """ 返されるものはメソッド用のデコレータの予定。当然、メソッドを引数として取る。
        """
        def _method_deco(self, *margs, **mkw):
            """ メソッド(instance method)呼び出しなので、第一引数はself。
            """
            @fn_deco
            def _function(*args, **kw):
                """ でも、渡されたデコレータは関数用なので対応するために内部で関数を作る。
                  (selfは外側の関数_method_decoのクロージャがあるので参照可能)
                """
                return method(self, *args, **kw)
            return _function(*margs, **mkw)
        return _method_deco
    return method_deco

できた。上手く動いている。

class B(object):
    @coerce_method(inc)
    def square(self, n):
        return n * n

print B().square(10) # 121

docstringのこととか考えるとこのままじゃまずいのだけれど。