MOテクノロジー

技術をメモしていくブログ

Pythonのデコレータは算数っぽく考えるとわかりやすかった

デコレータはなかなか理解するのが難しかったんだが、算数っぽく考えると割とすんなり飲み込めたのでメモする。

前提としてPythonは、関数内で関数を呼び出せる、関数を引数や関数の戻り値とできることを確認しつつ、ちょっとずつ考えていく。

関数内で関数を呼び出す

main関数の中にsub関数を作って、main()の実行結果を出力する。

>>> def main():
...     def sub():
...         print('This is sub.')
...     return sub
...
>>> main()
<function main.<locals>.sub at 0x10ee6a280>

結果は関数オブジェクトが返ってくる。これは納得、main()でsubをリターンしているんだから。
ではsub関数の内部のprint()を実行するにはどうするか。
main()の戻り値=subの関数オブジェクトになるんだから、算数っぽく考えると
main() = subの両辺に()をプラスして、 main()() = sub()となるはず。

>>> main()()
This is sub.

おk。

関数に引数として関数を渡す

関数の引数に関数を指定できることを確認する。

>>> def main(func):
...     return func
...
>>> def sub(text):
...     return text
...
>>> main(sub)
<function sub at 0x10ee6a1f0>

main関数の引数をしてsub関数を指定すると、return funcで引数にしたオブジェクトがそのまま返ってくる。これも納得。
ではsub関数の処理を実行するには、算数っぽく考えると
main(sub) = sub の両辺に()をプラスして、 main(sub)('あつい') = sub('あつい')となるはず。

>>> main(sub)('あつい')
'あつい'

デコレータにしてみる

ここまで理解できればデコレータも余裕。
call関数をデコレートして、処理の前後に別の処理を入れてみる。

# デコレートする側
>>> def main(func):
...     def sub(text):
...         print('sub 1.')
...         func(text)
...         print('sub 2.')
...     return sub
...
# デコレートされる側
>>> def call(text):
...     print(text)
...
>>> main(call)
<function main.<locals>.sub at 0x102cef4c0>

main関数の引数にcall関数オブジェクトを指定する。
main(call) = subになるから、両辺に()をプラスして、 main(call)('あつい') = sub('あつい')となるはず。

>>> main(call)('あつい')
sub 1.
あつい
sub 2.

これによりcall関数の処理の前後に別の文字列を表示することができている。 これがデコレータ。
最後に、Pythonicに@マークを使って書き換えてみる。

>>> def main(func):
...     def sub(text):
...         print('sub 1.')
...         func(text)
...         print('sub 2.')
...     return sub
...
>>> @main
... def call(text):
...     print(text)
...
>>> call('なんでこんなにあついねん')
sub 1.
なんでこんなにあついねん
sub 2.

今までのことを踏まえると、call('text')はmain(call)('text')と同じということになる。
callの前後にmainの処理を差し込むイメージ。この考え方が適切なのかどうかは不明だが、なんとなく理解できた。

算数っぽくとかって書いたけど、そもそも算数って両辺に同じ数足しても問題ないんだっけ?とかなってる。何も覚えていない。