目次

目次

Python のデコレータについて理解した話

アバター画像
江藤 光
アバター画像
江藤 光
最終更新日2017/07/13 投稿日2017/07/13

先輩が読んでいた 『Effective Python 』を読んでいたのですが、 途中から何を書いているのかサッパリ分からなかったので、 レベルを一つ落として『入門 Python3 』で基礎から勉強しなおしている江藤です。 Python もう半年近くやってるんですが、まだ門の中にいるのかすら微妙なところです…

今回、Python のシンタックスシュガーのひとつである「デコレータ」という機能について勉強しました。

デコレータとは、関数の上に “@デコレータ名” を書くと、 その関数に対して色んなことをしてくれる機能です。

例えば、特定のWebページを描画する関数の上にデコレータ requires_login を付けると 関数が呼ばれたときに一緒にログインのチェックもしてくれる、 という機能が実現可能です。

@requires_login
def login_page():
    render_edit_page()

これは、以下と同じ動きをしてくれます。

def login_page():
    if is_authorized(current_user):
        render_edit_page()
    else:
        redirect_403_page()

このような書き方を利用したことは何度もあったのですが、 その中身はどうなっているのか分かっていませんでした。 この、requires_login って一体どんな中身を書いているのでしょう…? これって自分で作ろうと思ったら、何をしないといけないのでしょう…?

今回、『入門 Python3 』を読んで仕組みから理解しました。 この記事は勉強した内容のまとめです。

デコレータの正体

Python のデコレータが行っているのは、 関数の上書きです。 (Java的には上書きだと「Override」ですが、PythonのデコレータはServletの「Filter」に近いと思います。)

Pythonでは 関数も第一級オブジェクのため(今回はじめて知りました…)、関数を関数の引数や返り値として使うことが出来ます。 関数を関数の引数や返り値として使うことで何が出来るのか、を実際にコードで試しながら考えてみます。

ログインチェックの例は複雑すぎて分かりにくいので、 もうちょっとシンプルな例でいきます。

以下の関数 add_two_ints は引数に取った二つの整数を足します。

def add_two_ints(a: int, b: int):
    return a + b

add_two_ints(3, 5)

出力結果は…

  

なにも出ません。print 文がないので当然なのですが。 この関数を上書きして、計算結果を出力させるようにします。

def print_result(func):
    def new_func(*args, **kwargs):
        result = func(*args, **kwargs)
        print(result)
        return result
    return new_func

def add_two_ints(a: int, b: int):
    return a + b

add_two_ints = print_result(add_two_ints)
add_two_ints(3, 5)

(詳しくは割愛しますが、args, kwargsには、関数funcの引数リストが入っています)

print_result 関数で何が起こっているか上から見ていくと

  1. 引数として関数 func を取っている
  2. ローカル関数 new_func の中で func を実行しつつ、その返り値を出力している
  3. new_func の中で func の結果を返している(これがないと、上書き後の関数が値を返さなくなってしまいます)
  4. print_result の返り値として、ローカル関数 new_func を返している

という流れです。

そして、最後から二番目の行で、add_two_ints という関数に print_result の結果を代入することで、 add_two_ints の実行内容は new_func で定義した中身に上書きされます。

出力結果は…

8

と、計算結果を自動的に出力してくれるようになりました!

Python のデコレータとは、この代入処理を”アットマーク”を用いた構文で書いているだけで、 先ほどの例を次のように書き直すだけで、デコレータを使うことが出来ます

def print_result(func):
    def new_func(*args, **kwargs):
        result = func(*args, **kwargs)
        print(result)
        return result
    return new_func

@print_result
def add_two_ints(a: int, b: int):
    return a + b

# add_two_ints = print_result(add_two_ints)  <= いらないのでコメントアウト
add_two_ints(3, 5)

出力結果は…

8

と、先ほどと同じ結果が返ってきました。

関数を返す関数、というのがデコレータの正体です

ちょっとだけ応用

デコレータを複数つける

デコレータは複数付けることが出来るようなので、 引数を出力する関数も作り、デコレータとしてくっつけてみましょう。

def print_args(func):
    def new_func(*args, **kwargs):
        print(args)
        print(kwargs)
        return func(*args, **kwargs)
    return new_func

def print_result(func):
    def new_func(*args, **kwargs):
        result = func(*args, **kwargs)
        print(result)
        return result
    return new_func

@print_args
@print_result
def add_two_ints(a: int, b: int):
    return a + b

add_two_ints(3, 5)
print()
add_two_ints(a=3, b=5)
(3, 5)
{}
8

()
{'a': 3, 'b': 5}
8

これで、引数も表示されました。 ちなみにですが、この例で分かるように *args には 位置引数のタプルが、 **kwargs にはキーワード引数の辞書が入っています。

デコレータに引数を付ける

デコレータ自体に引数を渡して、それによって振る舞いを変えたいというときがあると思います。 デコレータにも引数を渡すことは可能です。

例として、引数にとったメッセージを処理の前後で表示する print_message を書いてみました。

def print_result(func):
    def new_func(*args, **kwargs):
        result = func(*args, **kwargs)
        print(result)
        return result
    return new_func

def print_message(start_message: str, end_message: str):
    def _print_args(func):
        def new_function(*args, **kwargs):
            print(start_message)
            result = func(*args, **kwargs)
            print(end_message)
            return result
        return new_function
    return _print_args

@print_message('処理開始', '処理終了')
@print_result
def add_two_ints(a: int, b: int):
    return a + b

add_two_ints(3, 5)

出力結果は

処理開始
8
処理終了

となります。

複数のデコレータを書いたときの処理の順番

上記の例を見て気付いた方がいらっしゃると思いますが、 「8」と「処理終了」の出力順番は必ずしもこうなるとは限りません。 どちらも “func の処理が終わったら” としか書いていないからです。

これは、仕様として決まっていて、 デコレータは 下に書かれているものから順番に実行されます。 一番下のデコレータで返ってきた結果が、 次のデコレータの func 引数として渡ります。

実際

@print_message('処理開始', '処理終了')
@print_result
def add_two_ints(a: int, b: int):
    return a + b

@print_result
@print_message('処理開始', '処理終了')
def add_two_ints(a: int, b: int):
    return a + b

とすると、出力結果も変わり

処理開始
処理終了
8

となります。

この順番の仕様を勘違いしていると、想定外の処理が起こる可能性があります。 (私は最初上から順に実行されると思っていて「あれ?思った結果と違う」となっていました…)

おわりに

デコレータを使うと、関数にコードを書き足すことなく、機能を追加することが出来ます。 「この処理、どこにでも出てきてるじゃん」、という処理や 「この処理、本来は関数中に含めない方がいいじゃん」という処理があれば、 今後はデコレータ化するという選択肢も考えながら設計が出来ればと思います。 (と同時に、なんでもデコレータ化したがる”デコレータ病”にならないように気をつけます…)

アバター画像

江藤 光

まだまだ気持ちは新人です。

目次