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

Python

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

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

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

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

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

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

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

デコレータの正体

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

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

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

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

出力結果は…

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

(詳しくは割愛しますが、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 で定義した中身に上書きされます。

出力結果は…

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

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

出力結果は…

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

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

ちょっとだけ応用

デコレータを複数つける

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

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

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

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

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

出力結果は

となります。

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

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

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

実際

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

となります。

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

おわりに

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

Python