はじめに
初投稿です。
過去 IT 講師をしておりましたが、今後は開発の案件に入る可能性が高いため、文章力を衰えさせないために技術の共有をしていきます。
Python の学習でまず困惑するのが、デコレータかと思います。便利かつ重要なトピックなので取り上げることにしました。
デコレータ
「関数の中身を変更せずに、処理を付け加える」というような解説が散見されますが、初心者には分かりにくいので可能な限り分かりやすく解説していきます。
デコレータ deco
と実質的な処理を記述するラッパー関数 wrapper
を考えます。
def deco(func):
def wrapper(*args, **kwargs):
...
return wrapper
*args
と **kwargs
で、どんな形式の引数も全て受け取れます
デコレータを定義する手順は以下の通りです。
- 関数を引数とする関数の定義
- その中にラッパー関数の定義
- ラッパー関数内に実際の処理の記述
- ラッパー関数自身の返却
関数を定義する際に @
が付与されたデコレータ名を上に記述することで、関数内の記述に手を加えることなく、処理を追加できます。
@deco
def func(args_𝟣, ...):
...
1. 繰り返しの防止
複数の関数が同様の処理を有する場合に、便利に使えます。
同様の処理を関数として独立させるイメージが近いでしょうか。
func_1
~ func_3
に、A
という処理を追加したいケースを考えます。
def func_𝟣(...):
...
def func_𝟤(...):
...
def func_𝟥(...):
...
以下のように関数内に A
と追記すれば、問題ないように思えます。
def func_𝟣(...):
A
...
def func_𝟤(...):
A
...
def func_𝟥(...):
A
...
これでは A
が何回も登場しており記述が冗長なので、デコレータで処理をまとめてみましょう。
def deco(func):
def wrapper(*args, **kwargs):
A
func(*args, **kwargs)
return wrapper
@deco
def func_𝟣(...):
...
@deco
def func_𝟤(...):
...
@deco
def func_𝟥(...):
...
サンプルコードなので A
は 1 行ですが、実際は複数行にわたる可能性があります。つまり A
の行数に比例して、デコレータの恩恵は大きくなります。
2. 保守性の向上
他のメリットも確認していきましょう。
前の例だと A
に変更が生じた際に、func_1
~ func_3
内の 3 箇所の A
を触る必要があります。これでは影響箇所が大きくなり、修正が大変です。
そこで共通の処理をデコレータ内で定義すると、A
の変更による影響を最低限に抑えられます。deco
内の A
を変更するだけで、func_1
~ func_3
の挙動を変更できます。
それぞれの関数を触る必要がなくなるため、保守性・拡張性の向上が見込めます。
def deco(func):
def wrapper(*args, **kwargs):
A
func(*args, **kwargs)
return wrapper
@deco
def func_𝟣(...):
...
@deco
def func_𝟤(...):
...
@deco
def func_𝟥(...):
...
3. 可読性の向上
追加される処理に名前を付けられるため、分かりやすくなるということです。
Web アプリケーションで用いられる Flask の関数で説明します。
@app.route("/hello")
def hello():
return "Hello, World!"
実際の処理の内容はさておき、デコレータ名からルーティングに関する処理だということが予想できます。
このメリットを享受するためにも、デコレータ名はふさわしいものを付けるべきです。
実例
関数が少なくメリットが見えづらいですが、記述自体はそこまで難しくないです。
def deco(func):
def wrapper(*args, **kwargs):
print("START")
func(*args, **kwargs)
print("END")
return wrapper
@deco
def hello():
print("Hello")
hello()
--> START
Hello
END
応用的な記述法
複数のデコレータ
デコレータを複数適用させるには、単純に続けて記述するだけですが、順序が重要となります。
def func_1(func):
def wrapper(*args, **kwargs):
print("A")
func(*args, **kwargs)
print("B")
return wrapper
def func_2(func):
def wrapper(*args, **kwargs):
print("X")
func(*args, **kwargs)
print("Y")
return wrapper
まずは func_1
➩ func_2
で適用させます。
@func_1
@func_2
def msg_print_1(msg):
print(msg)
msg_print_1("message")
--> A
X
message
Y
B
デコレータの記述を逆にします、結果を予想してみてください。
@func_2
@func_1
def msg_print_2(msg):
print(msg)
msg_print_2("message")
--> X
A
message
B
Y
デコレータといえども関数であることには変わりないので、デコレータを組み合わせて複雑な処理を記述できます。
引数付きデコレータ
デコレータが引数を受け取る場合ですが、ネストが深くなり複雑に見えます。
def deco(some):
def actual_deco(func)
def wrapper(*args, **kwargs):
...
return wrapper
return actual_deco
some
はデコレータが受け取った引数で、actual_deco
は今までのデコレータ定義と同じです。
引数で与えた数から、関数実行前にカウントダウンを表示するデコレータを考えます。
def countdown(n=3):
def actual_deco(func):
def wrapper(*args, **kwargs):
nonlocal n
while n:
print(n)
n -= 1
func(*args, **kwargs)
return wrapper
return actual_deco
デコレータに引数を与えて関数を実行すると、問題なく表示されます。
@countdown(5)
def print_msg(msg):
print(msg)
print_msg("Hello")
--> 5
4
3
2
1
Hello
デコレータにはデフォルト引数を与えているので、引数を省略できます。ただし、()
は省略できません。
@countdown()
def print_msg(msg):
print(msg)
print_msg("Hello")
-->
3
2
1
Hello
おわりに
分かりにくい箇所や誤りあれば、コメントをお願いします。