先輩が読んでいた 『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 関数で何が起こっているか上から見ていくと
- 引数として関数 func を取っている
- ローカル関数 new_func の中で func を実行しつつ、その返り値を出力している
- new_func の中で func の結果を返している(これがないと、上書き後の関数が値を返さなくなってしまいます)
- 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 |
となります。
この順番の仕様を勘違いしていると、想定外の処理が起こる可能性があります。
(私は最初上から順に実行されると思っていて「あれ?思った結果と違う」となっていました…)
おわりに
デコレータを使うと、関数にコードを書き足すことなく、機能を追加することが出来ます。
「この処理、どこにでも出てきてるじゃん」、という処理や
「この処理、本来は関数中に含めない方がいいじゃん」という処理があれば、
今後はデコレータ化するという選択肢も考えながら設計が出来ればと思います。
(と同時に、なんでもデコレータ化したがる”デコレータ病”にならないように気をつけます…)
この記事を書いた人
- まだまだ気持ちは新人です。
最近書いた記事
- 2018.03.23Windows のコンソールを使いやすくしよう
- 2018.02.23GitHubでPullRequestが出ると、Jenkinsでテストした後でEC2に自動デプロイする設定を行った
- 2018.02.21Jenkins にパラメータを渡して、Packer で引数付きビルドを行う
- 2018.01.10それ、キーボードマクロで出来ますよ(Emacs)