先輩が読んでいた 『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
となります。
この順番の仕様を勘違いしていると、想定外の処理が起こる可能性があります。 (私は最初上から順に実行されると思っていて「あれ?思った結果と違う」となっていました…)
おわりに
デコレータを使うと、関数にコードを書き足すことなく、機能を追加することが出来ます。 「この処理、どこにでも出てきてるじゃん」、という処理や 「この処理、本来は関数中に含めない方がいいじゃん」という処理があれば、 今後はデコレータ化するという選択肢も考えながら設計が出来ればと思います。 (と同時に、なんでもデコレータ化したがる”デコレータ病”にならないように気をつけます…)
江藤 光
まだまだ気持ちは新人です。