初心者にはわかりにくいデコレータ
頭に@ooo
つけるやつ。初心者だと聞いたことあるけど、なんか処理を付け足せるらしいくらいしかわかっていないと思うので解説。
初心者勉強用の資料に書きます。
デコレータとは名前の通り、デコレーションする者ってこと。
まずは超簡単な関数を準備。
def deco1():
print('テストです')
deco1()
$ deco1.py
テストです
これは解説するまでもないですね。
事前知識: 関数もオブジェクト
Pythonの関数って実はオブジェクト。intとかstr型みたいに変数に代入することもできるし、引数として渡すこともできます。
def sample():
print('さんぷる')
func = sample
func()
$ sample.py
さんぷる
func
と言う変数に入れた後に()
と言うのをつけると関数というのは実行されることになる。
1. デコレータの原型作る
これにadd_before_after_str()
の定義を追加
def add_before_after_str(f):
def before_after_str():
print('開始')
f()
print('終了')
return before_after_str
これで関数を返す関数を作成できます。
コード説明
- add_before_after_str の引数
f
と言うのが引数としてもらう関数 -
before_after_str()
が関数内関数- 関数の中でしか使えない関数
-
before_after_str()
の中、引数にはf
というものが定義されていない -
before_after_str()
より外側のadd_before_after_str
にあるf
つまり、引数としてもらった関数が使われる
-
f()
のように()
がついたときに引数としてもらった関数を実行する。- 「開始」を表示
- 引数の関数
f
を実行 - 「終了」を表示
-
before_after_str
を返却- つまり
()
をつければ実行される関数(オブジェクト)が返却
- つまり
1.1 デコレータの原型を実行
def add_before_after_str(f):
def before_after_str():
print('開始')
f()
print('終了')
return before_after_str
def deco1():
print('テストです')
func = add_before_after_str(deco1)
func()
実行してみると
$ python3 deco1.py
開始
テストです
終了
ついでに、いちいち関数を変数に代入して、実行。と書かなくても1行で書くことも可能です。
# 省略すると↓で書ける
add_before_after_str(deco1)()
普通はこんなふうに書かないので少し分かりにくいですが、一応。
コード説明
-
deco1
という関数をadd_before_after_str
に引数として渡す -
func
という変数には()
をつければセットされてる関数の処理が実行される状態のものが入っている -
func()
とすることで処理を実行- add_before_after_strの引数
f
にdeco1
関数が渡る - 開始 表示
-
deco1
関数実行 - 終了 表示
- add_before_after_strの引数
となる。
「開始」「終了」を付け足せるデコレータの原型ができた。
add_before_after_str()
を実行しているけど、処理的にはbefore_after_str()
としてるということになってる。
ついでに、add_before_after_str(deco1)()
とすると1行で書ける。わかりにくいけど。
2. デコレータつける
@
つけるのがデコレータなのにこれまで@
全然でてきてませんね。なので書き換えてみる。
def add_before_after_str(f):
def before_after_str():
print('開始')
f()
print('終了')
return before_after_str
@add_before_after_str
def deco2():
print('テストです')
deco2()
$ python3 deco2.py
開始
テストです
終了
原型で作ったものとやっている処理は同じ。
「開始」「終了」を付け足せるデコレータができている。
ただ、deco2
という関数を実行していて、その処理に特定の処理を付け足すと言う意味は分かりやすくなってる。
3. 引数ありのデコレータ作成
先ほどのデコレータだと引数のある関数では対処できません。
もう一度デコレータを取って、分かりやすい形にして書き直します。
見ていただければ分かりますが「!」を語尾につけるだけの関数に、「開始」「終了」を付け足せると言うだけのものです。
def add_before_after_str(f):
def before_after_str(_t):
print('開始')
f(_t)
print('終了')
return before_after_str
def deco3(text):
print(text + '!')
# functionというオブジェクトをfuncに入れる
func = add_before_after_str(deco3)
print(type(func))
func('テストです')
$ python3 deco3.py
<class 'function'>
開始
テストです!
終了
コード説明
-
func = add_before_after_str(deco3)
でdeco3関数をセット-
()
つけるまでは実行はされていない -
func
と言う変数にはdef before_after_str(_t)
が入っている事になる -
テストです
と言う文字列の引数を与えると引数_t
に渡り、実行される -
f()
は事前にセットしたdeco3()
-
引数が混乱しますが、このような動作。
デコレータに改造
説明は終わったのでデコレータにします。
def add_before_after_str(f):
def before_after_str(_t):
print('開始')
f(_t)
print('終了')
return before_after_str
def deco3(text):
print(text + '!')
@add_before_after_str
def deco3(text):
print(text + '!')
deco3('テストです')
$ python3 deco3.py
開始
テストです!
終了
やっていることは前回と同じですね。
add_before_after_str
からdeco3()
を実行するような形になっていますがadd_before_after_str
の処理を付け足す感じがわかりやすくかけてる。
4. 汎用性を高める
先ほどまでの例だと、どんな関数でもいいから「開始」「終了」をつけるのが引数の数が一致せずできない。
全く便利でないので可変長引数を使って引数の数が変わっても汎用的に使える形に改造
def add_before_after_str(f):
def before_after_str(*args, **kwargs):
print('開始')
f(*args, **kwargs)
print('終了')
return before_after_str
@add_before_after_str
def deco4(num1, num2):
print(f'{num1} - {num2} = {num1-num2}')
@add_before_after_str
def deco4_2():
print('サンプル')
deco4(123, num2=100)
print('-------------')
deco4_2()
$ python3 deco4.py
開始
123 - 100 = 23
終了
-------------
開始
サンプル
終了
二つの引数の数が異なる関数をデコレータつけて実行。
どちらも問題なく動いてる。
コード説明
- 引数を与えられた場合
*args
と書くとargs
と言う変数にタプルで複数の値入る事になる- deco4の引数にてアンパックされるので
num1, num2 = (123, 100)
をやっているような感じになる - 引数ない場合も何も起こらないのでエラーでない
- deco4の引数にてアンパックされるので
-
**kwargs
はdictで与えた場合にdictでkwargs
に入る。 -
deco4(123, num2=100)
のようにキーワード引数使うこともできるが名前は一致させること
5. デコレータで戻り値を返す
処理を付け足すことはできました。場合によっては戻り値をつける必要があるので簡単に触れます。
def add_before_after_str(f):
def before_after_str(*args, **kwargs):
print('開始')
_r = f(*args, **kwargs)
print('終了')
return _r
return add_start_end
@add_before_after_str
def deco5(text):
print('deco5実行')
return text + '!'
result = deco5('テストです')
print(result)
$ python3 deco5.py
開始
deco5実行
終了
テストです!
コードを見ればなんとなく理解はできるかなと思います。「!」をつけた文字列を返す処理の前後に処理を挟む場合はこのように書けばOK。
最後に帰ってきた値をprintしている。
何に使うか
初心者だと何に使うか不明なデコレータですが、これは何に使うかというと、
- 何回も同じ処理を書く必要がなくなる
- 使われている例だとDjangoで
@login_required
など。- ログインが必要なページを表示する処理などのメソッドの上につければ、それだけでOK
- 一目でこの処理はログイン必要な処理だとわかる
from django.contrib.auth.decorators import login_required
@login_required
def new_article(request, pk):
...
user名とpasswordを見て、一致していたらnew_article()
と言う新しく記事を作成する処理を行わせることができるなど。
user名とpasswordを見る処理をいちいち書くのは面倒だが、このようなデコレータを使うとコードをスッキリと分かりやすく、再利用しやすい形で書くことができる