python のデコレーター構文
@decorate

解り難い @decorate 構文

Python のデコレーターは理解するのが厄介です。Python のデコレーターは syntax sugar の機能と closure の機能の二つを使って関数オブジェクトを修飾する構文です。でも closure の概念がない C/C++ や Java などの 言語から python も使うようになった者に、三重にネストした関数と @decorator 構文を組み合わせたコード例を使って解説されたのでは脳みそが沸騰するだけです。

私は @decorator を使ったコード例の python virturla machine コードを追跡することで、やっとこさデコレーターを理解しました。私の味わされた苦労を軽減してもらうため、この web page をまとめてみます。

デコレーターを理解した後で考え直してみると、デコレーターの説明が解り難いのは syntax sugar の説明と closure の説明が一緒になってしまっているせいだと思われます。以下ではこれらを分けて、まず単純な syntax sugar の説明の後に closure の説明をしていきます。

デコレーターの syntax sugar

python のデコレーターは closure に syntax sugar を かぶせたものだと言われます。closure とは何かというの面倒な説明は後回しにします。 まず @decorate 構文での syntax sugar が関数の置き換えであることを理解しましょう。

@decorate 構文の syntax sugar == 関数の置き換え

ブロック実行

下に示すような //@@ と //@@@ で囲んだコードを何度も使います。これは //@@ と //@@@ で囲んだ部分の文字列を \#####.### の名前のファイルにし、//@@@ の次に続く // で始まる行の文字列をコンソール・コマンド文字列として実行するという意味です。下の例では //@@ から //@@@ の範囲の文字列を \#####.### ファイルにし、そのファイルをカレント・ディレクトリの a.cpp にコピーし、a.cpp を cl.exe でコンパイルする処理を意味します。

//@@
    ・
    ・
//@@@
//copy \#####.### a.cpp
//cl a.cpp -I.\

このような処理をエディタで自動的に行うようにしてあります。wz エディタでのマクロで実装しています。ctrl O + E で、この一連の処理を行わせています。GPL 条件で公開していますので、wz エディタを使える方はご利用ください

python のときは、実行オプションが付かないことが大部分なので「 //@@ ... //@@@ 文字列を copy \#####.### temp.py と copy させ、カレント・ディレクトリで python.exe temp.py を実行させる」ことまでを ctrl O + P に割り振ったエディタ・マクロで行わせています。sf でのブロック式を計算させるときは 「temp.se を作り sf.exe @@temp.se を実行させる」ことまでを ctrl + O S に割り振ったエディタ・マクロで行わせています。

以下では //@@ と //@@@ で囲んだプログラム・コードが何度も出てきますが、このように自動的にテンポラリ・ファイルを作って実行する意味であることを御承知しておいてください。

@decorate 構文での syntax sugar とは「関数を置き換える記述を簡潔に表す」構文を意味しています。closure を使わない、関数を入れ替えるだけの意味を強調した次のようなコードを書けます。

//@@
def dcrtF(objAg):
    print "Now in dcrtF(.):",objAg
    return "string returned by dcrtF(.) function"

@dcrtF
def testF( objAg ):
    print "Now in testF(.):", objAg

print "--------------"
print testF # testF is string, not function
//@@@
Now in dcrtF(.): <function testF at 0x0091FC30>
--------------
string returned by dcrtF(.) function

        ---- コード例 1 ----

上のように @dcrtF 構文で引数が存在しないとき、修飾される関数 testF オブジェクトを dcrtF(.) 関数の引数として与えます。上の例では dcrtF(testF) と呼び出します。もし dcrtF() 関数が引数を一つも持たない関数のときは「TypeError: dcrtF() takes no arguments (1 given)」と コンパイル・エラーになります。

下のように引数を持つ @dcttF(..) 構文も可能ですが、この説明は少し複雑なので後回しにします。

    ・
@dcrtf("test string")
def testF( objAg ):
    ・

なお、上のコードの置き換えの様子は python のデバッグ・モードでも追跡できます。コードの意味が解り難いと思ったときは、デバッグ・モードで一行ずつトレースしてみてください。python がデコレーター構文でやっていることを直接見れます。print type(testF) などをデバッガ上で実行させれば testF ラベルが文字列オブジェクトを指すように書き換えられている様子を確認できます。

通常の実用的なデコレーター構文では、dcrtF 関数は callable object を返します。例えば testF ラベルの指し示す先を別の関数オブジェクトにすることで、デコレーター構文での修飾の後でも testF(.) 呼び出しが可能にしておきます。

//@@
def testF2( objAg ):
    print "Now in testF2(.):", objAg

def dcrtF(fnAg):
    print "Now in dcrtF(.):",fnAg
    return testF2

@dcrtF
def testF( objAg ):
    print "Now in testF(.):", objAg

print "--------------"
testF(5)
//@@@
Now in dcrtF(.): 
--------------
Now in testF2(.): 5

        ---- コード例 2 ----

python のデコレーター構文では、通常は引数として渡された関数に 別の機能を追加した関数を返すことで関数を修飾します。そのために dcrtF(.) の引数は、修飾される関数オブジェクトになっているわけです。今度は python の decorator らしく渡される関数オブジェクトに機能を追加した関数オブジェクトを返します。具体的には関数の実行時間を計測する機能で修飾してみます。今までの python decorator の syntax sugar が理解できていれば難しくありません。まだ関数の置き換えにすぎません。 closure 機能を使いません。

//@@
fnBufferStt=None
def testF2( objAg ):
    import time
    print "Now in testF2(.) and start testF(objAg):", objAg
    dbStartTimeAt = time.clock()
    fnBufferStt(objAg)
    print time.clock() - dbStartTimeAt

def dcrtF(fnAg):
    global fnBufferStt
    print "Now in dcrtF(.):",fnAg
    fnBufferStt = fnAg
    return testF2

@dcrtF
def testF( objAg ):
    print "Now in testF(.):", objAg, "   sum up:", sum(range(1000))

print "--------------"
testF(5)
//@@@
Now in dcrtF(.): 
--------------
Now in testF2(.) and start testF(objAg): 5
Now in testF(.): 5    sum up: 499500
0.000105235562917

        ---- コード例 3 ----

ここでの @dcrF による修飾は任意の関数に適用できます。任意の関数の実行時間を計測できます。ただし一つの関数しかデコレートできません。 fnBufferStt が一つしかないからです。別の関数をデコレートすると fnBufferStt が その別の関数オブジェクトに入れ替わってしまうからです。このような問題を回避できるように closure 機能を使います。


デコレーターの closure 機能

dcrtF(.) 関数の内側に関数をネストさせることで、上の fnBufferStt グローバル変数では一つの関数しか修飾できない問題を奇麗に回避できます。これが closure 機能を利用したことになっています。

//@@
def dcrtF(fnAg):
    print "Now in dcrtF(.):",fnAg

    def innerF(objAg):
        import time
        print "Now innerF(.)", objAg
        dbStartTimeAt = time.clock()
        fnAg(objAg)
        print time.clock() - dbStartTimeAt

    return innerF

@dcrtF
def sumUp( inAg ):
    print "Now in sumUp(.):", inAg, "   sum up:", sum(range(inAg))

import numarray as sc
@dcrtF
def sumUpByNum( inAg ):
    #import numarray as sc
    print "Now in testFF(.):", inAg, "   sum up:", sum(sc.arange(inAg))

print "--------------"
sumUp(10)
print "--------------"
sumUpByNum(10)
print "--------------"
sumUp(100000)
print "--------------"
sumUpByNum(100000)
//@@@
Now in dcrtF(.): 
Now in dcrtF(.): 
--------------
Now innerF(.) 10
Now in sumUp(.): 10    sum up: 45
1.20745304432e-005
--------------
Now innerF(.) 10
Now in testFF(.): 10    sum up: 45
0.000293212441695
--------------
Now innerF(.) 100000
Now in sumUp(.): 100000    sum up: 4999950000
0.0165474221147
--------------
Now innerF(.) 100000
Now in testFF(.): 100000    sum up: 4999950000
0.0797975989624

        ---- コード例 4 ----

closure オブジェクト

上のコード例で dcrtF(.) 関数が 「return innnerF 」と innerF 関数オブジェクトを返すように書かれていますが、そのように読み取ったら誤りです。実際には innerF 関数オブジェクトに fnAg 引数を追加した 下の四角で囲った部分の fnAg と innerF の組である closure オブジェクトが返されています。

          ┌──┐
def dcrtF(│fnAg│):
          └──┘
    print "Now in dcrtF(.):",fnAg
  ┌───────────────────┐
  │def innerF(objAt):                    │
  │    import time                       │
  │    print "Now innerF(.)", objAt      │
  │    dbStartTimeAt = time.clock()      │
  │    fnAg(objAt)                       │
  │    print time.clock() - dbStartTimeAt│
  └───────────────────┘
    return innerF

        ---- closure オブジェクト ----

fnAg と innerF の組である closure オブジェクトを返すことで、「コード例 3」の fnBufferStt 変数を無くせたわけです。 「クラスはデータにコードを追加したものであり、一方で closure はコードにデータが追加したものだ」と言われることがあります。上の例では innerF コードに fnAg データが追加された innerF closure が返されています。

「closure が関数とどのように違うのかをか」「closure になるとなぜ fnAg が保存されるのか」を、ここでは説明しません。稿を改めて「dis/inspect モジュールを使った Python のハッキング」で python virtual machine code/C ソース・コード ceval.c にまで遡って詳しく説明します。興味のある方は、こちらも読んでみてください。

python コンパイラが innerF(.) の中で dcrtF(.) 側のローカル変数を参照していることを検出することで closure オブジェクトを生成し 「return innerF」で、その closure オブジェクトを返します。innerF(.) から dcrtF(.) へのローカル変数の参照がなければ、closure オブジェクトの生成は発生しません。単純に innerF 関数オブジェクトを返すだけです。

callable オブジェクトによる closure オブジェクトの代用

ここまで読んで、「 closure ではなく callable な class instance でも同じことができるじゃないか」と思った方は鋭い感性をお持ちです。そのとおりです。「コード例 4」の関数実行時間の計測機能のデコレーションは、下のクラス・インスタンスで作った callable object を使ったデコレーションでも実現できます。

//@@
class ClMesureTime:
    def __init__(self, fnObjAg):
        self.m_fnObj = fnObjAg
    
    def __call__(self, objAg):
        import time
        print "Now ClMesureTime.__call__(.)", self.m_fnObj
        dbStartTimeAt = time.clock()
        self.m_fnObj(objAg)
        print time.clock() - dbStartTimeAt
        

def dcrtF(fnAg):
    print "Now in dcrtF(.):",fnAg
    return ClMesureTime(fnAg)

@dcrtF
def sumUp( inAg ):
    print "Now in sumUp(.):", inAg, "   sum up:", sum(range(inAg))

import numarray as sc
@dcrtF
def sumUpByNum( inAg ):
    #import numarray as sc
    print "Now in testFF(.):", inAg, "   sum up:", sum(sc.arange(inAg))

print "--------------"
sumUp(10)
print "--------------"
sumUpByNum(10)
print "--------------"
sumUp(100000)
print "--------------"
sumUpByNum(100000)
//@@@
Now in dcrtF(.): 
Now in dcrtF(.): 
--------------
Now ClMesureTime.__call__(.) 
Now in sumUp(.): 10    sum up: 45
1.18485445611e-005
--------------
Now ClMesureTime.__call__(.) 
Now in testFF(.): 10    sum up: 45
0.000273038836647
--------------
Now ClMesureTime.__call__(.) 
Now in sumUp(.): 100000    sum up: 4999950000
0.0195182090744
--------------
Now ClMesureTime.__call__(.) 
Now in testFF(.): 100000    sum up: 4999950000
0.0717133042329

        ---- コード例 5 ----

ただし別の「dis/inspect モジュールを使った Python のハッキング」で詳しく説明するように closure のほうが単純な python virtual machine コードになります。効率的なコードになります。callable なクラス・インスタンスを設けるには class の生成とクラスに付属する dict 変数を生成せねばなりません。closure ならば innerF 関数オブジェクトに inAg 変数を追加するだけで済みます。

closure オブジェクト, callable オブジェクトどちらで修飾するかはプログラマーの総合的な判断に委ねられます。私自身は closre オブジェクト記述のための内側の関数とのネストが解り難くなったときは、callable class instance() にすべきだと思っています。



@decorate 構文での引数

@decorate 構文には引数を与えられます。より汎用的な修飾が可能です。ただし引数を与えられたときは、decorate(引数) の戻り値が callable であることを前提に、一つの @decorate(.) 構文に対し、二回の関数呼び出しが発生します。 一回目の decorate(引数)が戻した callable オブジェクトに対して、修飾される関数を引数として渡して二回目の呼び出しを行います。二回目の関数呼び出しの戻り値で修飾される関数を置き換えます。

今度は「コード例 4],「コード例 5」で @dtrtF(strComment) とコメント文字列を引き渡せるようにしたテスト・コードを使います。

callble な class instance による引数付きデコレーション

最初は簡単な方の @dcrtF(引数) 構文が callable class instance を返すコード例を示します。
//@@
class ClMesureTime:
    def __init__(self, strCommentAg):
        self.m_strComment = strCommentAg
    
    def __call__(self, fnObjAg):
        print "Now ClMesureTime.__call__(.)", fnObjAg
        self.m_fnObj = fnObjAg
        return self.__decorate

    def __decorate(self, objAg):
        import time
        print "Now __decrate(.):"+self.m_strComment+": ", objAg
        dbStartTimeAt = time.clock()
        self.m_fnObj(objAg)
        print time.clock() - dbStartTimeAt


def dcrtF(strCommentAg):
    print "Now in dcrtF(.):",strCommentAg
    return ClMesureTime(strCommentAg)


@dcrtF("sumUp function")
def sumUp( inAg ):
    print "Now in sumUp(.):", inAg, "   sum up:", sum(range(inAg))

import numarray as sc
@dcrtF("sumUpByNum function")
def sumUpByNum( inAg ):
    #import numarray as sc
    print "Now in sumUpByNum(.):", inAg, "   sum up:", sum(sc.arange(inAg))

print "--------------"
sumUp(10)
print "--------------"
sumUpByNum(10)
print "--------------"
sumUp(100000)
print "--------------"
sumUpByNum(100000)
//@@@
Now in dcrtF(.): sumUp function
Now ClMesureTime.__call__(.) 
Now in dcrtF(.): sumUpByNum function
Now ClMesureTime.__call__(.) 
--------------
Now __decrate(.):sumUp function:  10
Now in sumUp(.): 10    sum up: 45
1.10453717792e-005
--------------
Now __decrate(.):sumUpByNum function:  10
Now in sumUpByNum(.): 10    sum up: 45
0.000272711082288
--------------
Now __decrate(.):sumUp function:  100000
Now in sumUp(.): 100000    sum up: 4999950000
0.0206723813325
--------------
Now __decrate(.):sumUpByNum function:  100000
Now in sumUpByNum(.): 100000    sum up: 4999950000
0.0772189977801

        ---- コード例 6 ----

closure による引数付きデコレーション

「コード例 6」の機能を closure によって記述します。引数つきのデコレーション構文を使うため、三重の関数ネストによる closure を使います。なれないうちは頭がくらくらすると思います。

//@@
def dcrtF(strCommentAg):
    print "Now in dcrtF(.):",strCommentAg

    def innerF(fnObjAg):
        def innerInnerF(objAg):
            import time
            print "Now innerF(.):"+strCommentAg+": ", objAg
            dbStartTimeAt = time.clock()
            fnObjAg(objAg)
            print time.clock() - dbStartTimeAt

        return innerInnerF

    return innerF

@dcrtF("sumUp function")
def sumUp( inAg ):
    print "Now in sumUp(.):", inAg, "   sum up:", sum(range(inAg))

import numarray as sc
@dcrtF("sumUpByNum function")
def sumUpByNum( inAg ):
    print "Now in testFF(.):", inAg, "   sum up:", sum(sc.arange(inAg))

print "--------------"
sumUp(10)
print "--------------"
sumUpByNum(10)
print "--------------"
sumUp(100000)
print "--------------"
sumUpByNum(100000)
//@@@
Now in dcrtF(.): sumUp function
Now in dcrtF(.): sumUpByNum function
--------------
Now innerF(.):sumUp function:  10
Now in sumUp(.): 10    sum up: 45
1.15217879325e-005
--------------
Now innerF(.):sumUpByNum function:  10
Now in testFF(.): 10    sum up: 45
0.000276609712903
--------------
Now innerF(.):sumUp function:  100000
Now in sumUp(.): 100000    sum up: 4999950000
0.0149708742611
--------------
Now innerF(.):sumUpByNum function:  100000
Now in testFF(.): 100000    sum up: 4999950000
0.0790338734379
        ---- コード例 7 ----


実用的な @decorate 構文の例

より実用的な @decorate 構文を扱いましょう。ここ:「関数やメソッドの修飾構文」で説明されている、関数の引数タイプをチェックするデコレーターです。三重の関数ネストによる closure を使っていますが、ここまでのデコレーター構文の「関数置き換え:syntax sugar」と closure が理解出ていれば、理解できるはすです。

//@@
import math

#def declareArgs():
def declareArgs(*argTypes):

    def checkArguments(func):   # @declareArgs() のときに働く
        assert func.func_code.co_argcount == len(argTypes)

        def wrapper(*args, **kwargs):
            pos = 1
            for (arg, argType) in zip(args, argTypes):
                assert isinstance(arg, argType), \
                        "Value %r dose not match %s at %d" % (arg, argType, pos)
                pos += 1
            return func(*args, **kwargs)
        #wrapper.func_name = func.func_name 
        return wrapper
    #import dis
    #dis.dis(checkArguments)
    return checkArguments   # checkArgments 関数オブジェクトを返している

@declareArgs(float, float)
def calcDistance(x, y):
    return math.sqrt(x * x + y * y)

print "------------"
print calcDistance(3.14, 1.592)
print "------------"
print calcDistance(3.14, "abc")
//@@@
------------
3.52052041607
------------
Traceback (most recent call last):
  File "temp.py", line 32, in ?
    print calcDistance(3.14, "abc")
  File "temp.py", line 15, in wrapper
    assert isinstance(arg, argType), \
AssertionError: Value 'abc' dose not match  at 2

        ---- コード例 8 ----

私が最初上のコードと その解説を見たとき「理解できない」、「何か重要な抜けがある」と感じました。そのため、上のコードに対応する python virtual machine コードを追跡して closure オブジェクトの説明がされていないことに気付きました。「return checkArguments」や「return wrapper」が関数オブジェクトを返していると解釈してしまっていたのでは、上のコードが解るはずありません。皆様はどうでしょうか。