読者です 読者をやめる 読者になる 読者になる

Pythonのデコレータを理解するときに残したメモ

python

Pythonのデコレータ(decorator)を理解する 3 - -を1,2,3全て読んで、以前僕がデコレータを理解するために残したメモもこんな感じだったなというのを思い出した。ずっと記事にしようと思いつつ忘れてたんだけどせっかくなので加筆修正しつつ公開してみる。

引数無しのデコレータ

デコレータとは、その名の通り元の関数を装飾するもの。別の言い方をすれば、元の関数そのものを弄ること無く挙動を変更するもの。

まずは単純なデコレータから。

def hello(function):
    def _hello(*args, **kw):
        result = function(*args, **kw)
        return "Hello, {0}".format(result)
    return _hello

@hello
def name(arg):
    """my docstring"""
    return arg

# これは以下と同義。実際デコレータ式が導入される前はこうしていた。
def name(arg):
    """my docstring"""
    return arg

name = hello(name)

# 実行
print name('John Doe')
  • デコレータ付ける前:name('John') # 'John'
  • デコレータ付けた後:name('John') # 'Hello, John'

元の関数nameは単純に与えられた引数をそのまま返すだけ。そこにデコレータhelloを付けることによって、"Hello, xxxx"のような文字列を返すように挙動が変更される。デコレータ式じゃない方を見てみるとわかるように、関数を返す関数helloの引数として関数nameを渡し、返ってきた関数をnameに再代入する感じ。

ところで、これだと元の関数名がデコレータの内側の関数名で上書きされてしまう。

name.func_name # '_hello'
name.func_doc  # None

これを解決するためには、functoolモジュールのwrapsを使う。

from functools import wraps

def hello(function):
    @wraps(function)
    def _hello(*args, **kw):
        return function(*args, **kw)
    return _hello

@hello
def name(arg):
    """my docstring"""
    return arg

これで以下のように関数名と関数のdocstringが正しく取得できるようになる

name.func_name # 'name'
name.func_doc  # 'my docstring'

デコレータ自体に引数を与える

flaskの簡単なview関数

@app.route('/')
def hello():
    return "Hello World!"

こんな風にデコレータ自体に引数を渡したい場合はどうするか。

この場合、

  • デコレート対象の関数の引数を受ける関数
  • デコレート対象の関数自身を引数として受け取る関数
  • デコレータ自身の引数を受け取る関数

が必要になるので、少なくとも3重にネストした関数になる。

from functools import wraps


def hello(friend1, friend2):
    def _hello(function):
        @wraps(function)
        def __hello(*args, **kw):
            result = function(*args, **kw)
            return "Hello, {0} & {1}! I'm {2}.".format(friend1, friend2, *args, **kw)
        return __hello
    return _hello


@hello("Jack", "Mary")
def name(arg):
    return arg


# デコレータ式を使わない場合
# 式の左側の変数名はイメージしやすいように_helloとかnameを使ったけれど別になんでもいい。
_hello = hello("Jack", "Mary")
name = _hello(name)


print name("John")  # 'Hello, Jack & Mary! I'm John.'