2008年10月9日木曜日

Python のデコレータ式 (1)

1. デコレータの仕組みについて知りたい

Python を使っていて、これまでに所々でデコレータを目にしてきた。

デコレータの仕組みについて理解してないので、そろそろ確認しておくことにする。

 

デコレータと等価な関数定義

7.5 関数定義 には、デコレータを使った関数定義と、それと同等の式の例が書かれている。

関数定義は一つまたは複数のデコレータ式 (decorator expression) でラップできます。…

例えば、以下のようなコード:

@f1(arg)
@f2
def func(): pass

は、

def func(): pass
func = f1(arg)(f2(func))

と同じです。

これを元に、いくつか例を考えてみる。

 

2. デコレータを適用したときに文が実行される、何もしないデコレータ

a. デコレータ式を使わない場合

次の関数を定義する。

  1. 文字列 “hoge” を出力する、関数 hoge を定義。
  2. 関数を引数として受け取る、関数 D を定義。この関数は、関数を受け取ると、print 文を実行する他は何もせず、呼び出されたときに与えられた関数を、そのまま返す。

この 関数 D がデコレータの役割をする。 (これをデコレータと言ってしまうのは、名が体を表わしてないけれど ^^;)

def D(f):
    print u"デコレータが実行された"
    return f

def hoge():
    print "hoge"

# hoge 関数をデコレート
hoge = D(hoge)
hoge()

変数 hoge に、関数 hoge にデコレータ関数 D を適用した結果を代入。そして hoge() を実行。結果は、

デコレータが実行された
hoge

デコレートする側の関数 D が実行されるタイミングに注目すると、hoge 関数を D に渡したときに実行されているのがわかる。

 

b. デコレータ式を使う場合

上記は、次のデコレート式を使った場合と同じ。

def D(f):
    print u"デコレータが実行された"
    return f

# デコレータを適用したときに、関数 D が適用される。
@D
def hoge():
    print "hoge"

hoge()

先ほどと同じく、

@D

で、デコレーターが適用されたとき、デコレータとなる関数 D が実行される。hoge() の呼出しでデコレータとなる関数が実行されるわけではない。

 

3. ネストした関数を利用し、デコレータを適用しただけでは、何もしないように変更

a. デコレータ式を使わない場合

次に、デコレータを適用した時点で、デコレータ関数が実行されないように変更する。

そのためには、ネストした関数を利用すれば良い。

以下では、デコレータ関数 D の中に、関数 _ を定義した。普通は、適当な名前を付けると思うけれど、見やすいので、便宜的に関数名にアンダーバーを利用した。

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

def hoge():
    print "hoge"

# デコレータ関数 D を適用する前
print "before decoration: "
hoge()

# デコレータ関数 D を 関数 hoge に適用
hoge = D(hoge)

# デコレータ関数 D を hoge に適用した後
print "after decoration: "

hoge()

結果は、

before decoration: 
hoge
after decoration: 
*--*--*--*--*--*--*--*--*--*--
hoge
--*--*--*--*--*--*--*--*--*--*

デコレートする前の hoge() の呼出しでは、hoge 関数の内容が、そのまま出力された。

デコレートした後では、出力に「飾り」がついている。

また、デコレータを適用した時点では、先ほどのように、デコレータ関数の中身が実行されていない。理由は、デコレータ D に渡された関数 hoge は、ネストした関数の中で実行される。しかし、デコレータ関数を hoge 関数に適用したときに返されるのは、ネストした関数 _ であるため、その時点では実行されない。

後の hoge() の呼び出しにより、ネストした関数 _ が実行される。その時点で、デコレートされて hoge 関数が実行される。

もちろん、デコレータなので、関数 D は hoge 関数をデコレートする専用の関数ではない。例えば、上記のコードの下に、次のように piyo 関数を書いて、piyo をデコレートしてみる。

def piyo():
    print "piyo"
    
piyo = D(piyo)

piyo()

結果、piyo もデコレートされる。

*--*--*--*--*--*--*--*--*--*--
piyo
--*--*--*--*--*--*--*--*--*—*

このとき、hoge, piyo をデコレートするネストした関数 _ は別々のオブジェクト。

 

b. デコレータ式を使う

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

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

@D
def hoge():
    print "hoge"

hoge()

@D
def piyo():
    print "piyo"
    
##piyo = D(piyo)

piyo()

 

4. デコレータを利用して、本質的な処理と、飾りを分ける

関心の分離

アスペクト指向的な点を強調するなら、

次のような、コードがデコレータの役割を端的に表している。

def D(f):
    def _():
        print u"▲▲▲ 事前処理 ▲▲▲"
        f()
        print u"▼▼▼ 事後処理 ▼▼▼"
    return _

@D
def hoge():
    print u"■■■"
    print u"■■■ hoge の本質的な処理"
    print u"■■■"

hoge()

@D
def piyo():
    print u"■■■"
    print u"■■■ piyo の本質的な処理"
    print u"■■■"

piyo()

結果は、

▲▲▲ 事前処理 ▲▲▲
■■■
■■■ piyo の本質的な処理
■■■
▼▼▼ 事後処理 ▼▼▼
▲▲▲ 事前処理 ▲▲▲
■■■
■■■ piyo の本質的な処理
■■■
▼▼▼ 事後処理 ▼▼▼

ただし、デコレータ式の場合、アスペクトを挿入したい関数の前に記述する必要がある。

Aspect-oriented programming - Wikipedia, the free encyclopedia に列挙されているライブラリでは、その辺りが柔軟に操作できそう。

 

5. デコレートする関数の中で、関数のプロパティを利用する

関数の特殊属性を使えば、デコレータの中で、関数の名前などのプロパティを取得できる。

def D(f):
    def _():
        print "before: " + f.__name__ + "()"
        f()
        print "after: " + f.__name__ + "()"
    return _

@D
def hoge():
    print "hoge"

hoge()

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

 

関連サイト

を知っていれば、デコレータ式は、その関数版で、シンプルな記法により、お手軽に利用できるツ-ル、と言った感じかな。