Pythonのデコレータ(decorator)を理解する 3

承前:Pythonのデコレータ(decorator)を理解する 2

デコレータに引数を渡す

素晴らしい、ではデコレータ自身に引数を渡すにはどうしたらよいのだろうか? さて、デコレータは引数として関数を受け入れる必要があるのでちょっとやっかいだ。デコレータに直接デコレートされた関数-引数を渡すことはできないからだ。
解決を急ぐ前に、ちょっと復習をしよう。

# デコレータは'普通'の関数である
def my_decorator(func):
    print "I am a ordinary function"
    def wrapper():
        print "I am function returned by the decorator"
        func()
    return wrapper

# そのため、なにも"@"など使わずに呼び出すことができる

def lazy_function():
    print "zzzzzzzz"

decorated_function = my_decorator(lazy_function)
# => I am a ordinary function

# それは"I am a ordinary function"と出力する。
# なぜならそれはあなたが行った通りだからだ:
# 関数を呼び出す. 魔法は何もない。

@my_decorator
def lazy_function():
    print "zzzzzzzz"

# => I am a ordinary function

それは全く同じだ。my_decoratorは呼び出されている。そして@my_decoratorとしたときは、Pythonに変数my_decoratorでラベル付けされた関数を呼べと指示している。それが重要なのだ、なぜならあなたが与えたラベルはデコレータを直接指す(point)ことができるからだ……いずれにせよ! 邪悪になることから始めよう!

def decorator_maker():
    #私はデコレータたちを作る! 私は一度しか実行されない:
    #あなたが私にデコレータを作らせる時に。
    print "I make decorators! I am executed only once: "+\
          "when you make me create a decorator."

    def my_decorator(func):
        #私はデコレータ! 関数をデコレートするときだけに実行される。
        print "I am a decorator! I am executed only when you decorate a function." 

        def wrapped():
            #私はデコレータされた関数のラッパー。
            #私はデコレートされた関数が呼ばれた時に呼ばれる。
            #ラッパーとして、私はデコレータされた関数の結果を返す。
            print ("I am the wrapper arround the decorated function. " 
                  "I am called when you call the decorated function. " 
                  "As the wrapper, I return the RESULT of the decorated function.")
            return func()
        #デコレータとして、私はラップされた関数を返す
        print "As the decorator, I return the wrapped function."

        return wrapped
    # デコレータ製造機として私はデコレータを返す。
    print "As a decorator maker, I return a decorator"
    return my_decorator

# デコレータを作ろう。それは結局はただの新しい関数だ。
new_decorator = decorator_maker()       
#出力:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator

# それから関数をデコレートする

def decorated_function():
    print "I am the decorated function."

decorated_function = new_decorator(decorated_function)
#出力:
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function

# 関数を呼び出そう:
decorated_function()
#出力:
#I am the wrapper arround the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

驚くべきことは何もない。さあこれと全く同じ事をやってみよう、ただし中間にある変数を飛ばして。

def decorated_function():
    print "I am the decorated function."
decorated_function = decorator_maker()(decorated_function)
#出力:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function.

# 最後に:
decorated_function()    
#ouputs:
#I am the wrapper arround the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

もう一度同じ物を作ろう、更に短く。

@decorator_maker()
def decorated_function():
    print "I am the decorated function."
#出力:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function.

#結局こうなる: 
decorated_function()    
#出力:
#I am the wrapper arround the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

よし、ここまでで我々は"@"構文を使って関数を呼び出したのが分かったはずだ:-) ところで引数付きデコレータの話に戻ろう。もしその場でデコレータを生成する関数を使うことができるなら、その関数に引数を渡すことができるんじゃないだろうか?

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):

    print "I make decorators! And I accept arguments:", decorator_arg1, decorator_arg2

    def my_decorator(func):
        # ここにおいて引数を渡す機能は、クロージャの賜物である。
        # クロージャに慣れていない場合は以下の記事を読むといい。
        # http://stackoverflow.com/questions/13857/can-you-explain-closures-as-they-relate-to-python
        print "I am the decorator. Somehow you passed me arguments:", decorator_arg1, decorator_arg2

        # デコレータの引数と関数の引数を混同しないように!
        def wrapped(function_arg1, function_arg2) :
            print ("I am the wrapper arround the decorated function.\n"
                  "I can access all the variables\n"
                  "\t- from the decorator: {0} {1}\n"
                  "\t- from the function call: {2} {3}\n"
                  "Then I can pass them to the decorated function"
                  .format(decorator_arg1, decorator_arg2,
                          function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapped

    return my_decorator

@decorator_maker_with_arguments("Leonard", "Sheldon")
def decorated_function_with_arguments(function_arg1, function_arg2):
    print ("I am the decorated function and only knows about my arguments: {0}"
           " {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments("Rajesh", "Howard")
#出力:
#I make decorators! And I accept arguments: Leonard Sheldon
#I am the decorator. Somehow you passed me arguments: Leonard Sheldon
#I am the wrapper arround the decorated function. 
#I can access all the variables 
#   - from the decorator: Leonard Sheldon 
#   - from the function call: Rajesh Howard 
#Then I can pass them to the decorated function
#I am the decorated function and only knows about my arguments: Rajesh Howard

見ればわかるように、このトリックを使って任意の関数のようなデコレータに引数を渡すことができる。必要に応じて*argsや**kwargsも使うことができる。しかしデコレータはたった一度しか呼ばれないことを覚えておいて。Pythonスクリプトをインポートする時だけだ。その後には動的に引数をセットすることはできない。"import x"を実行した時、関数はもうすでにデコレートされており、従って何かを変更することはできない。

レッツ・プラクティス:デコレータをデコレートするデコレータ

オーケー、オマケとして、いかなる引数も総称的に受け取るデコレータを作るコードスニペットを与えよう。結局のところ、引数を受け取るために、我々は別の関数を使ってデコレータを生成した。我々はデコレータをラップした。何か最近ラップされた関数を見た? ええもちろん、デコレータだ! 楽しみながらデコレータのためのデコレータを書いてみよう。

def decorator_with_args(decorator_to_enhance):
    """ 
    この関数はデコレータとして使われるはずである。
    よって他の関数をデコレートしなければならない、それはデコレータとして使われることを意図する。
    一杯のコーヒーを取って。
    あらゆるデコレータは任意の数の引数を受け取る事ができ、
    毎回それをどのようにして使うかという悩みからあなたを守るのだ。
    """

    # デコレータに引数を渡す方法と同じトリックを使う。
    def decorator_maker(*args, **kwargs):

        # 関数のみを受け取るデコレータをその場で生成する。
        # しかし生成器から渡された引数はキープしておく。
        def decorator_wrapper(func):

            # 元のデコレータの結果を返す、それは結局ただの関数だ(関数を返す)
            # 一つだけ注意: デコレータは具体的にはこのような書き方(signature)でなければ動かない:
            return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper

    return decorator_maker

これは以下のように使うことができる。

# あなたはデコレータとして使うために関数を作った。そしてその上にデコレータを突っ込むのだ :-)
# あと忘れないで、書き方は"decorator(func, *args, **kwargs)"だよ。
@decorator_with_args 
def decorated_decorator(func, *args, **kwargs): 
    def wrapper(function_arg1, function_arg2):
        print "Decorated with", args, kwargs
        return func(function_arg1, function_arg2)
    return wrapper

# さて、あなたはあなたの謹製のデコレートされたデコレータとともに、所望の関数をデコレートできる。

@decorated_decorator(42, 404, 1024)
def decorated_function(function_arg1, function_arg2):
    print "Hello", function_arg1, function_arg2

decorated_function("Universe and", "everything")
#出力:
#Decorated with (42, 404, 1024) {}
#Hello Universe and everything

# ヒューー!

たしかに、これまであなたはデコレータについて、ある人が次のように言うのを聞いたような印象を受けるだけだっただろう:「再帰を理解する前に、まず再帰を理解しなければならない」。しかし今では、デコレータを習得するのは気分の良いことだと分かっていただけただろうか?

デコレータを利用する際のベストプラクティス

  • デコレータ構文はPython 2.4以降で動作する。*1
  • デコレータは関数呼び出しを遅くする、これを覚えておこう。
  • 関数をアンデコレートすることはできない。デコレータを除去するデコレータを作るというハックがあるが誰もそれを使わない。一度関数がデコレートされたらすべてのコードに対してそれが実行されるからだ。
  • デコレータは関数を包むので、デバッグを難しくする。

デバッグの問題は、Python 2.5(以降)において提供されるfunctoolsに含まれるfunctools.wrapによって解決される。それはそのラッパーのために包まれたすべての関数の名前、モジュールおよびdocstringをコピーする。興味深いことに、functools.wrapはデコレータなのだ :-)

# デバッグのために、スタックトレースは関数__name__を表示する。
def foo():
    print "foo"

print foo.__name__
#出力: foo

# デコレータはデバッグをややこしくする。
def bar(func):
    def wrapper():
        print "bar"
        return func()
    return wrapper

@bar
def foo():
    print "foo"

print foo.__name__
#出力: wrapper

# "functools"は以下のようにこれを解決する。

import functools

def bar(func):
    # 我々はそれを"ラッパー"と呼んだ、"func"をラップするからだ。
    # そして魔法が始まる。
    @functools.wraps(func)
    def wrapper():
        print "bar"
        return func()
    return wrapper

@bar
def foo():
    print "foo"

print foo.__name__
#出力: foo

どのようにしたらデコレータを役立てることができるのか?

根本的な疑問として、何に対してデコレータを使うことができるのだろう? クールでパワフルに見えるが、しかし実例も素晴らしいのだ。可能性は無限であるといっても過言でない。典型的な用途は外部ライブラリから関数の振る舞いを拡張すること(あなたはその関数自体を変更できないから)またはデバッグ目的(デバッグは一時的なものだからコードを変更したくない)である。DRY原則を実現するために、コードを毎回書き換えずに、同じコードで複数の関数を拡張するために、デコレータを使うことができる。例:

def benchmark(func):
    """
    このデコレータは関数の実行にかかった時間を表示する。
    """
    import time
    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print func.__name__, time.clock()-t
        return res
    return wrapper


def logging(func):
    """
    このデコレータはスクリプトの動作を記録する。
    (ここではprintしているだけだけど、loggingすることもできるよ!)
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print func.__name__, args, kwargs
        return res
    return wrapper


def counter(func):
    """
    このデコレータは関数の実行された回数をカウントして表示する。
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print "{0} has been used: {1}x".format(func.__name__, wrapper.count)
        return res
    wrapper.count = 0
    return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
    return str(reversed(string))

print reverse_string("Able was I ere I saw Elba")
print reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!")

#出力:
#reverse_string ('Able was I ere I saw Elba',) {}
#wrapper 0.0
#wrapper has been used: 1x 
#ablE was I ere I saw elbA
#reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {}
#wrapper 0.0
#wrapper has been used: 2x
#!amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A

当然のことだが、デコレータの良いところは、書き換えることなくほとんどあらゆる事にすぐさまそれらを使うことができるということである。それをDRYと呼んだ:

@counter
@benchmark
@logging
def get_random_futurama_quote():
    import httplib
    conn = httplib.HTTPConnection("slashdot.org:80")
    conn.request("HEAD", "/index.html")
    for key, value in conn.getresponse().getheaders():
        if key.startswith("x-b") or key.startswith("x-f"):
            return value
    return "No, I'm ... doesn't!"

print get_random_furturama_quote()
print get_random_furturama_quote()

#出力:
#get_random_futurama_quote () {}
#wrapper 0.02
#wrapper has been used: 1x
#The laws of science be a harsh mistress.
#get_random_futurama_quote () {}
#wrapper 0.01
#wrapper has been used: 2x
#Curse you, merciful Poseidon!

Pythonはそれ自身でいくつかのデコレータを提供している:プロパティ、スタティックメソッド、などなど。Djangoはキャッシュとビューパーミッションを管理するためにデコレータを利用している。Twistedは非同期関数呼び出しのインライン化するふりをする(訳注:自信なし*2)。デコレータは本当に大きな遊び場だ。┃

*1:修正
原文:They are new as of Python 2.4, therefore be sure that's what your code is running on.
「それ(デコレータ構文)はPython 2.4において新しく追加されたので、あなたのコードが動作することを確認しましょう」
実行環境が2.3以下でないかという確認だと思われるので訳文は簡単にした

*2:Twisted to fake inlining asynchronous functions calls