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

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

デコレータ詳説

先の例に対して、デコレータ構文を使うと次のようになる:

@my_shiny_new_decorator
def another_stand_alone_function():
    print "Leave me alone"

another_stand_alone_function()  
#出力:  
#Before the function runs
#Leave me alone
#After the function runs

そう、これで全部、シンプルだ。@decoratorは単なる、

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

のショートカットにすぎない。Decoratorはデザインパターンにおけるwikipedia:Decorator パターンのPythonicな変形にすぎないのだ。Pythonにはiteratorのように開発を容易にするための古典的デザインパターンがいくつか組み込まれている。

当然だが、デコレータは積み重ねることができる。

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "<\______/>"
    return wrapper

def ingredients(func):
    def wrapper():
        print "#tomatoes#"
        func()
        print "~salad~"
    return wrapper

def sandwich(food="--ham--"):
    print food

sandwich()
# => --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#出力:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

Pythonのデコレータ構文を使うと次のようになる:

@bread
@ingredients
def sandwich(food="--ham--"):
    print food

sandwich()
#出力:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

(デコレータを書く)順序はデコレータが何であるかを決定する。

@ingredients
@bread
def strange_sandwich(food="--ham--"):
    print food

strange_sandwich()
#出力:
##tomatoes#
#</''''''\>
# --ham--
#<\______/>
# ~salad~

結局のところ質問に答えると

結論として、あなたはどのようにこのStackOverFlow.comの質問(最初の質問)に答えればいいか簡単に分かるでしょう。

# ボールド(太字)にするためのデコレータ
def makebold(fn):
    # デコレータを返す新しい関数
    def wrapper():
        # 前後にいくつかのコードを挿入する
        return "<b>" + fn() + "</b>"
    return wrapper

# イタリックにするためのデコレータ
def makeitalic(fn):
    # デコレータを返す新しい関数
    def wrapper():
        # 前後にいくつかのコードを挿入する
        return "<i>" + fn() + "</i>"
    return wrapper

@makebold
@makeitalic
def say():
    return "hello"

print say() 
# => <b><i>hello</i></b>

# これは実際には次に等しい
def say():
    return "hello"
say = makebold(makeitalic(say))

print say() 
# => <b><i>hello</i></b>

これであなたは幸せのうちにここを去ることもできますが、少し知恵を絞ることによって応用的なデコレータの利用法を知ることもできます。

デコレートされた関数に引数を渡す

# これは黒魔術ではない、あなたのラッパーは単に引数を
# 渡すことができなければならない:

def a_decorator_passing_arguments(function_to_decorate):
    def a_wrapper_accepting_arguments(arg1, arg2):
        print "I got args! Look:", arg1, arg2
        function_to_decorate(arg1, arg2)
    return a_wrapper_accepting_arguments

# デコレータによって返された関数を呼び出している時点で
# 引数を渡すと、ラッパーを呼び出してそれがデコレートされた
# 関数に引数を渡すようになる

@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
    print "My name is", first_name, last_name

print_full_name("Peter", "Venkman")
# 出力:
#I got args! Look: Peter Venkman
#My name is Peter Venkman

メソッドをデコレートする

Pythonの素晴らしいところは、メソッドの第一引数が現在のオブジェクト(self)への参照を持つことを期待されているという点を除けば、メソッドと関数が実際には同じものであるということだ。それはメソッドが(関数と)同じ方法でデコレータを構築できるということを意味するが、selfを取るという点を忘れずに覚えておこう。

def method_friendly_decorator(method_to_decorate):
    def wrapper(self, lie):
        lie = lie - 3 # とても親切だ、勝手にサバを読んでくれる :-)
        return method_to_decorate(self, lie)
    return wrapper


class Lucy(object):

    def __init__(self):
        self.age = 32

    @method_friendly_decorator
    def sayYourAge(self, lie):
        print "I am %s, what did you think?" % (self.age + lie)

l = Lucy()
l.sayYourAge(-3)
# => I am 26, what did you think?

当たり前だが、あなたが極めて汎用的なデコレータを作って任意の関数やメソッドに適用することを望む場合は、引数の数に依存しない、*argsや**kwargsを利用するとよいだろう。

def a_decorator_passing_arbitrary_arguments(function_to_decorate):
    # ラッパーは任意の引数を受け入れる
    def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
        print "Do I have args?:"
        print args
        print kwargs
        # ここでは引数*args, **kwargsを展開している。
        # 展開に慣れていない方は以下のURLを参照:
        # http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
        function_to_decorate(*args, **kwargs)
    return a_wrapper_accepting_arbitrary_arguments

@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print "Python is cool, no argument here."

function_with_no_argument()
#出力
#Do I have args?:
#()
#{}
#Python is cool, no argument here.

@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
    print a, b, c

function_with_arguments(1,2,3)
#出力
#Do I have args?:
#(1, 2, 3)
#{}
#1 2 3 

@a_decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, platypus="Why not ?"):
    print "Do %s, %s and %s like platipus? %s" %\
    (a, b, c, platypus)

function_with_named_arguments("Bill", "Linus", "Steve", platypus="Indeed!")
#出力
#Do I have args ? :
#('Bill', 'Linus', 'Steve')
#{'platypus': 'Indeed!'}
#Do Bill, Linus and Steve like platipus? Indeed!

class Mary(object):

    def __init__(self):
        self.age = 31

    @a_decorator_passing_arbitrary_arguments
    def sayYourAge(self, lie=-3): # You can now add a default value
        print "I am %s, what did you think ?" % (self.age + lie)

m = Mary()
m.sayYourAge()
#出力
# Do I have args?:
#(<__main__.Mary object at 0xb7d303ac>,)
#{}
#I am 28, what did you think?

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