本記事について
Pythonのデコレータの理解に滅茶苦茶苦しんだので、備忘録的にメモを残します。
もし誰かの役に立てば幸いです。
全2話予定で、本記事は1話目です。
想定する読者
本記事は「デコレータを学んだけど理解できず折れた人」向けの内容となっています
というのも、デコレータの理解の前提になるクロージャ、関数内関数、高階関数などの説明は省略しているためです。
注意事項
・デコレータの理解を優先していること及び筆者の力量不足から一連のサンプルコードは全く実践的なものではありません。申し訳ございません。
・タイトルに「トライアンドエラー」とある通り、
こうしたい⇒修正してみた⇒あれうまくいかないぞ⇒原因はこれだ⇒うまくいった!
という流れを繰り返します。
くどいくらい繰り返します。
他の人に理解してもらうため…というより、そのくらいしないと自分が理解できなかったからです。
要領の良い人にとっては苦痛かと思いますので、飛ばし飛ばしお読みください。
LESSON1 上司からのリクエスト~デコレートは突然に~
さて、突然ですがあなたが管理するプロジェクトには実行すると「こんにちは:base_funcから出力」と出力する関数base_funcをはじめ、似たような動作をする関数が30個ほどあるとします。(社内では「base_funcシリーズ」と呼ばれています)
def base_func1():
return "こんにちは:base_func1から出力"
result = base_func1()
print(result)
def base_func2(msg):
return msg+':base_func2から出力'
result = base_func2("こんばんは")
print(result)
#以下、似たような関数がbase_func3~30まで存在。(省略)
この関数に何の意味があるかはさておき、ある日あなたは上司から指示を受けました。
上司「関数base_func1~basefunc30を実行したとき、出力の前後に★マークがつくようにしてほしい。いわゆるデコレーションってやつだね。とにかくデコってほしいんだ。」
この指示に何の意味があるかはさておき、早速とりかかることにしました。
LESSON2 原始的な対応(print関数に手を加えてみる)
あなたは一番最初にprint(result)
の箇所に★を追加する方法を思いつきました。
たとえばbase_func1
で試すとこんな感じ。
def base_func1():
return "こんにちは:base_func1から出力"
result = base_func1()
print("★"+result+"★")
★こんにちは:base_func1から出力★
はい、たしかに上司の要望には応えました。
がしかし、ちょっとダサいです…
(そもそも関数base_func1の機能自体がダサいのは触れないでおきましょう)
LESSON3 少しテクニカルな対応(高階関数)
要望に応えたとはいえ、そのダサさに打ちのめされたあなたは思いつきました。
「そういえば、Pythonは関数を引数で受け渡しできたはず。関数base_func1
を引数で受けて、base_func1
の出力の前後に★マークをつけるための関数、いわばデコレート用の関数を作ってスタイリッシュにできないかな?」
いわゆる高階関数ですね。ネットで調べて早速実装してみました。
できあがったのはコチラ。
def deco1(get_func): #デコレーション用に新規作成した関数
return "★" + get_func() + "★"
def base_func1():
return "こんにちは:base_func1から出力"
result = deco1(base_func1)
print(result)
★こんにちは:base_func1から出力★
おぉ、print関数を直接いじるよりもなんだか高度な感じになりましたね。
ここまできたらあとは
result = deco1(base_func2)
print(result)
result = deco1(base_func3)
print(result)
result = deco1(base_func4)
print(result)
#(以下略)
とひたすら関数base_func2~30まで関数deco1に投入すれば良さそうですね。
しかし、あなたはちょっと不満です。その理由はコチラです。
result = deco1(base_func1)
今までは
result = base_func1()
result = base_func2()
result = base_func3()
(以下略)
と書いていたので、それらをすべて
result = deco1(base_func1)
result = deco1(base_func2)
result = deco1(base_func3)
(以下略)
に修正しなくてはいけませんね。
うーん、関数deco1
はあくまで関数base_func
をデコるためのものなので、立ち位置はbase_func
のサブ的なところ。
それなのに、deco1(base_func1)
と関数deco1
のために修正しないといけない…。
なんだかスッキリしませんね。
理想は
result = base_func1()
この書き方は一切変えず、base_func1()
と今まで通り呼び出し実行するだけで関数deco1
も良い感じに実行され、★がついてくれることです。
お待たせしました。ここでデコレータの登場です!
LESSON4 デコレータ登場
デコレータを使うことで、
理想は
result = base_func1()
この書き方は一切変えず、base_func1()
と今まで通り呼び出し実行するだけで関数deco1
も良い感じに実行され、★がついてくれることです。
これが実現できちゃいます。
方法は簡単。@deco1
を関数base_func1
の上に書くだけ!
def deco1(get_func):
return "★" + get_func() + "★"
@deco1 #デコレータを追加!
def base_func1():
return "こんにちは:base_func1から出力"
result = base_func1
print(result)
★こんにちは:base_func1から出力★
どうでしょう!
見事、デコレータを使うことでresult = deco1(base_func1)
なんて厄介な書き方はせずにすみました!
さて、いったいどうして@deco1
を関数base_func1
の上に書くだけでこのような結果になったのでしょうか。
結論としては、
@deco1 #デコレータを追加!
def base_func1():
と書かれた状態では、
result = base_func1
…①
が、
result = deco1(base_func1)()
…②
に変換されるです。(「同義になるから」と言った方が良い?)
②は、先ほどあなたが不満に思ったコードですよね。でも、デコレータ(@)を使わない状況では②の書き方しかなかった。
それがデコレータを使用した今、本来、関数base_func1
を呼び出し実行するときと同じ書き方で★マークを前後につけてデコレートすることができるようになりました。
※そもそもデコレータを使用することで①が②と同義になる理由はここでは「そういうお約束」と思っていてください。
(自分も勉強中なので「糖衣構文」ってことくらいしか理解していません)
LESSON5 カンの良い人にはバレていたことでしょう…
さてさて、これであとはいよいよすべての関数base_func1~30
に@deco1
をつけて
@deco1 #デコレータを追加!
def base_func1():
#(中略)
@deco1 #デコレータを追加!
def base_func2():
#(中略)
@deco1 #デコレータを追加!
def base_func3():
#(中略)
#以下、base_func30まで@deco1をつけていく…
とすれば終わりですね!
と言いたいところですが、カンの良い人はもう気付いていることでしょう。ちょっとした気持ち悪さがあることに。
もう一度サンプル3のコードを見てみましょう。
result = base_func1
はい、ココ。
もともと右辺はbase_func1()
だったのに、呼び出し演算子()が取れてbase_func1
にしれっと変わっていました。
変えた理由は、base_func1()
と呼び出し演算子をつけたままだとエラーが起きるからです。
なぜか?トレースしてエラーが起きる原因を探ってみましょう。(画像はあくまでイメージです)
LESSON6 解決策を考えよう
呼び出し演算子をつけたままにするとエラーが起こる原因は分かりました。次は解決策を考えましょう。
なお、「呼び出し演算子が無しにすれば動くんでしょ?放置でよくね?はダメです。
場当たり的にbase_func1()
をbase_func1
に変えたことを「気持ち悪さ」なんてマイルドな(ふざけた)表現をしていますが、本来呼び出し演算子をつけて実行すべき関数base_func1
をつけずに実行するのはミスリードひいては事故の原因になります。
結論としては、「deco1関数の中に関数を作成する」ことで解決します。
百聞は一見に如かず。早速コードを確認しましょう。
def deco1(get_func):
def inner_deco1(): #新規作成した関数!
return "★" + get_func() + "★"
return inner_deco1
@deco1
def base_func1():
return "こんにちは:base_func1から出力"
#呼び出し演算子()付きでの実行は、サンプル3でエラーになったけどこの例では正常に動作する!
result = base_func1() #もともとの書き方のままでOK!修正は不要!
print(result)
★こんにちは:base_func1から出力★
バッチリですね!
関数deco1
の中に関数inner_deco1
をつけると呼び出し演算子付きで関数base_func1
を実行できるようになった理由ですが、こちらもトレースするのが手っ取り早いです。
これであとは本当に
@deco1 #デコレータを追加!
def base_func1():
#(中略)
@deco1 #デコレータを追加!
def base_func2():
#(中略)
@deco1 #デコレータを追加!
def base_func3():
#(中略)
#以下、base_func30まで@deco1をつけていく…
とすれば終わりですね!
呼び出し演算子()をつけたままbase_func1
~base_func30
を呼び出せるので、デコレータの@をつけまくる以外に既存コードの修正は必要ないですから!
LESSON7 上司「もっとデコろうぜ!」
さて、残りの関数base_func2
~base_func30
にもデコレータ(@deco1)をつけようとしたところ、上司が通りがかりました。
上司「作業はどうだ?お、いいじゃないか」
上司「ほう、デコレータってのを使うとこんなにもスタイリッシュなコードでデコれるんだね」
上司「じゃあ、★の外にさらに顔文字(^o^)もつけちゃおう。イケるっしょ?よろしく!」
はい、追加案件がきました。
「その顔文字いらんやろ」と言いたいところですが、社内政治的な理由から反論はNGです。
でも安心してください。デコレータは複数使用することができます。
(^o^)という(今にも吹っ飛ばしたい)顔文字をデコレートするためのデコレータを作成し、デコレート対象の関数base_func1
に@をつけてあげればいいのです。
新しく作成するデコレータは関数@deco2
としましょう。
イメージはこんな感じです!
@deco2 #新規作成するデコレータ。(*^o^*)を出力する
@deco1
def base_func1():
return "こんにちは:base_func1から出力"
というわけで全貌を早速コードで確認しましょう!
def deco1(get_func):
def inner_deco1():
return "★" + get_func() + "★"
return inner_deco1
def deco2(get_func):
def inner_deco2():
return "(*^o^*)" + get_func() + "(*^o^*)"
return inner_deco2
@deco2
@deco1
def base_func1():
return "こんにちは:base_func1から出力"
result = base_func1() #サンプル5ではエラーになったけど、今回はOK!
print(result)
(*^o^*)★こんにちは:base_func1から出力★(*^o^*)
オッケーですね!(^o^)
念のため処理をトレースしてみましょう。
LESSON8 引数問題発生!
謎の顔文字をデコることに成功し、今度こそホントに残りの関数base_func2
~base_func30
にもデコレータ(@deco1と@deco2)をつける作業が完了しました。
しかし!
動作確認をしたところ関数base_func2
~base_func30
のうち、引数を必要とするものでエラーが発生しました。
たとえば1つの引数を必要とする関数base_func2
です。
def deco1(get_func):
def inner_deco1():
return "★" + get_func() + "★"
return inner_deco1
def deco2(get_func):
def inner_deco2():
return "(*^o^*)" + get_func() + "(*^o^*)"
return inner_deco2
@deco2
@deco1
def base_func2(msg): #base_func1は引数は必要なかったが、base_func2は1つ引数が必要
return msg + ":base_func2から出力"
result = base_func2("ニーハオ")
print(result)
Traceback (most recent call last):
File "/xxx/xxx/tmp.py", line 18, in <module>
result = base_func2("ニーハオ")
TypeError: deco2.<locals>.inner_deco2() takes 0 positional arguments but 1 was given
原因を探るためにトレースしてみましょう。(図4)
LESSON9 引数問題を解決するために
原因が分かったので解決策を考えましょう。
当然の話ですがすぐに思いつくのは関数innner_deco2
と、inner_deco2
から呼び出し実行される関数inner_deco1
の両方に仮引数を追加することですね。
def deco1(get_func):
def inner_deco1(rcv_msg): #仮引数を追加(関数inner_deco2から送られてくる引数を受け取る)
return "★" + get_func(rcv_msg) + "★" #引数を追加(ここでの関数get_funcは関数base_func2を実行するので、関数base_func2への引数となる)
return inner_deco1
def deco2(get_func):
def inner_deco2(rcv_msg): #仮引数を追加
return "(*^o^*)" + get_func(rcv_msg) + "(*^o^*)" #引数を追加(ここでのget_funcはinner_deco1を実行するので、inner_deco1への引数となる)
return inner_deco2
@deco2
@deco1
def base_func2(msg): #関数inner_deco2 ⇒ 関数inner_deco1と渡ってきた引数を受け取る
return msg + ":base_func2から出力"
result = base_func2("ニーハオ") #引数'ニーハオ'は、関数inner_deco2 ⇒ 関数inner_deco1 ⇒ 関数base_func2 と渡っていく
print(result)
(*^o^*)★ニーハオ:base_func2から出力★(*^o^*)
良いですね!
ただお気づきの方もいるとおり、これではまだ十分とは言えません。
サンプル7のコードは、「関数base_func1
~base_func30
が絶対に1つの引数を受け取ること」を前提としたものになっています。
そのため、たとえば
-
def base_func1()
←引数を1つも受け取らない関数 -
def base_func3("msg", "msg2")
←引数を2つ受け取る関数
これらはエラーになります。
「では、引数の数が0でも10でもいくつでも耐えられるように可変にすればいいのでは?」
と思った方、その通りです!
可変の実現するための書き方ですが、これはもう先に正解を示します。
関数deco1
と関数deco2
の仮引数及び引数に*args
**kwargs
を使用すればOKです。
こうすることでデコレート対象の関数(今回であれば関数base_func1
~base_func30
)の仮引数がいくつであろうと、問題なく動作します。
当然、関数base_func1
~base_func30
の仮引数の記述を変更する必要はありません。
def deco1(get_func):
def inner_deco1(*args, **kwargs): #仮引数を修正。*args, **kwargsを用いることで、関数innner_deco2からどんな引数が渡ってきても受け取れるようになっている。
return "★" + get_func(*args, **kwargs) + "★" #引数を修正。*args, **kwargsを用いることで、デコレート対象(関数get_func。今回の場合はbase_func1~3のいずれか)の仮引数が何であろうと問題なく渡すことができる
return inner_deco1
def deco2(get_func):
def inner_deco2(*args, **kwargs): #仮引数を修正。*args, **kwargsを用いることで、どんな引数が渡ってきても受け取れるようになっている。
return "(*^o^*)" + get_func(*args, **kwargs) + "(*^o^*)" #引数を修正(ここでのget_funcはinner_deco1を実行するので、inner_deco1への引数となる)
return inner_deco2
@deco2
@deco1
def base_func1():
return "こんにちは:base_func1から出力"
@deco2
@deco1
def base_func2(msg): #関数inner_deco2 ⇒ 関数inner_deco1と渡ってきた引数を受け取る
return msg + ":base_func2から出力"
@deco2
@deco1
def base_func3(msg, msg2): #関数inner_deco2 ⇒ 関数inner_deco1と渡ってきた引数を受け取る
return msg + msg2 + ":base_func3から出力"
result = base_func1()
print(result)
result = base_func2("ニーハオ") #引数'ニーハオ'は、関数inner_deco2 ⇒ 関数inner_deco1 ⇒ 関数base_func2 と渡っていく
print(result)
result = base_func3("Hello","チャオ") #引数'Hello'と'チャオ'は、関数inner_deco2 ⇒ 関数inner_deco1 ⇒ 関数base_func2 と渡っていく
print(result)
(*^o^*)★こんにちは:base_func1から出力★(*^o^*)
(*^o^*)★ニーハオ:base_func2から出力★(*^o^*)
(*^o^*)★Helloチャオ:base_func3から出力★(*^o^*)
オッケーです!
無事、デコレート対象の関数(base_func1
~base_func30
)の仮引数が0でも1でも10でもなんでも引数を渡すことができるようになりました!
上司からの与えられたミッションは無事完了です!
お疲れ様でした!
次回予告
あなたは今回の作業を通し、最低限のデコレータを実装することができるようになりました。
ホッとしたのもつかの間、翌日あなたはまたしても上司に呼ばれこう言われました。
上司「申し訳ないんだけど、またちょっとお願いがあってね。」
上司「昨日、(*^o^*)と★でデコる機能つけてもらったじゃん。(*^o^*)はなんかふざけてるみたいだからやっぱり消してもらいたいんだ。」
上司「あと、★なんだけど、今の時代多様性っていうのかな?〇とは▽とかいろんな記号に対応できるようにしたいんだよね!」
上司「てなわけで対応よろしく!君ならできるだろ?あのデコレータって機能でさ!」
繰り返しますが社内政治的な理由から反論はNGです。
あなたは爽やかに「了解です!」と返答し、取り掛かることにしました。