2008年10月15日水曜日

Python のデコレータ式 (2) - デコレータに引数があり、複数のデコレータを適用する場合

Python のデコレータ式 (1) のつづき

1. 前回のデコレータ式の復習

前回は、引数のないデコレータ式について試した。今回はデコレータ式に引数がある場合について考える。

最初に、引数のないデコレータについて復習する。

def D(f):
    def _():
        print "*--" * 10
        f()
        print "--*" * 10
    return _

@D
def hoge():
    print "hoge"

##hoge = D(hoge)

hoge()

コメントアウトしてあるコードは、デコレータ式を使わない場合の書き方。デコレータ式は、このシンタクティックシュガー。

デコレートしたときに、関数 D が実行され、デコレートした関数をラップした関数が返される。上記では、デコレートされた後、関数 hoge は、hoge 関数がセットされた、関数 D にネストされた `_ 関数’ を指す。

 

2. デコレータに引数がある場合

次に、デコレータに引数がある場合を考える。

デコレータに引数を渡せると、デコレートの仕方をカスタマイズできるようになる。上記の例で考えると、出力される “hoge” を囲む

*--*--*--*--*—

の長さを、引数によって指定できるようになる。

 

デコレータ式を使わない場合で考える

では、上記のデコレートする関数 D に引数を与えたい場合、どうすればいいのだろう?

デコレータ式を使わない関数の呼出しで考えてみる。例えば、以下のようにデコレートする関数 D を呼び出し、引数を与えることができると想定する。

hoge = D(5)(hoge)
hoge()

これは、デコレート関数 D は引数として `5’ を受け取り、 hoge 関数をラップしている。

この場合の呼出しを順に考えると、

  1. 関数 D に 5 を与える。
  2. 上記の関数呼び出しの結果として返される関数に、関数 hoge を与える。
  3. 関数 hoge を呼び出す。

の 3 段階となっている。

そのため、デコレート関数に引数を与えない場合の関数定義よりも、一段多くネストした関数が必要となる。

よって、以下のように記述すれば良い。

def D(arg):
    def _(f):
        def __():
            print "*--" * arg
            f()
            print "--*" * arg
        return __
    return _

def hoge():
    print "hoge"

hoge = D(5)(hoge)
hoge()

(今回も適当な関数名が思い付かなったので、返される関数名をアンダーバーとした。)

実行される順を考えると、

  1. 最初に、デコレート関数 D に 5 を与えることにより、D(arg) が呼出され、`_ 関数’ が返される。
  2. 次に、`_ 関数’に hoge 関数 を与えることにより、_(f) が呼出され `__ 関数’ が返される。

この 2 段階の呼出しによって、最初に引数 `5’ がセットされ、次に hoge 関数がセットされたことになる。最後に、hoge 変数はデコレータ関数の一番奥にネストされた `__ 関数’ を指した状態になる。

 

デコレータ式を使う場合

上記をデコレータ式で書き直すと、

def D(arg):
    def _(f):
        def __():
            print "*--" * arg
            f()
            print "--*" * arg
        return __
    return _

@D(5)
def hoge():
    print "hoge"

hoge()

デコレータ式を使わない場合と比べると、記述はシンプルになった。しかし、元の形を知らなければ、デコレータ関数 D との対応が全くわからない。デコレータ式は、あくまでも syntactic sugar 。

 

3. 複数のデコレータを適用する

次に、デコレータ式を複数適用する場合について考える。

最初は、引数のないデコレータ式を二つ重ねる例を試す。例えば、デコレータ関数 D に加えて、デコレータ関数 C を適用したい場合、次にように書く。

def C(f):
    def _():
        print "-#-"*10
        f()
        print "-#-"*10
    return _

def D(f):
    def _():
        print "--*"*10
        f()
        print "*--"*10
    return _

@C
@D
def hoge():
    print "hoge"

##hoge = C(D(hoge))
hoge()

実行した結果は、

-#--#--#--#--#--#--#--#--#--#-
--*--*--*--*--*--*--*--*--*--*
hoge
*--*--*--*--*--*--*--*--*--*--
-#--#--#--#--#--#--#--#--#--#-

コメントアウトしてある部分が、デコレータ式を使わない方法。

実行結果を見ると、上の方に書かれているデコレータ式が、外側になることがわかる。

 

4. 引数のあるデコレータ式を複数適用する

最後に、引数のあるデコレータ式を、複数適用する方法を書く。

def C(arg):
    def _(f):
        def __():
            print "-#-" * arg
            f()
            print "-#-" * arg
        return __
    return _

def D(arg):
    def _(f):
        def __():
            print "-—*" * arg
            f()
            print "*—-" * arg
        return __
    return _

@C(10)
@D(5)
def hoge():
    print "hoge"

##hoge = C(10)(D(5)(hoge))
hoge()

実行結果は、

-#--#--#--#--#--#--#--#--#--#-
--*--*--*--*--*
hoge
*--*--*--*--*--
-#--#--#--#--#--#--#--#--#--#-

ここまで来ると、ネストした関数の見た目が複雑になった。 (@_@;)

しかし、結局、基本的には、

デコレータ関数1(引数)(元の関数)

となっており、これがネストしているだけ。

デコレータ関数2(引数)(デコレータ関数1(引数)(元の関数))

Python のデコレータ式 (3) につづく…