0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

トライアンドエラーで学ぶPythonのデコレータ【その1】

Last updated at Posted at 2023-05-05

本記事について

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で試すとこんな感じ。

サンプル1
def base_func1():
    return "こんにちは:base_func1から出力"

result = base_func1()
print(""+result+"")
結果
★こんにちは:base_func1から出力★

はい、たしかに上司の要望には応えました。
がしかし、ちょっとダサいです…
そもそも関数base_func1の機能自体がダサいのは触れないでおきましょう

LESSON3 少しテクニカルな対応(高階関数)

要望に応えたとはいえ、そのダサさに打ちのめされたあなたは思いつきました。
「そういえば、Pythonは関数を引数で受け渡しできたはず。関数base_func1を引数で受けて、base_func1の出力の前後に★マークをつけるための関数、いわばデコレート用の関数を作ってスタイリッシュにできないかな?」

いわゆる高階関数ですね。ネットで調べて早速実装してみました。
できあがったのはコチラ。

サンプル2
def deco1(get_func): #デコレーション用に新規作成した関数
    return "" + get_func() + ""

def base_func1():
    return "こんにちは:base_func1から出力"

result = deco1(base_func1) 
print(result)
サンプル2の結果
★こんにちは: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に投入すれば良さそうですね。

しかし、あなたはちょっと不満です。その理由はコチラです。

サンプル2(抜粋)
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の上に書くだけ!

サンプル3
def deco1(get_func):
    return "" + get_func() + ""

@deco1 #デコレータを追加!
def base_func1():
    return "こんにちは:base_func1から出力"

result = base_func1
print(result) 
サンプル3の結果
★こんにちは:base_func1から出力★

どうでしょう!
見事、デコレータを使うことでresult = deco1(base_func1)なんて厄介な書き方はせずにすみました!
さて、いったいどうして@deco1関数base_func1の上に書くだけでこのような結果になったのでしょうか。

結論としては、

サンプル3(抜粋)
@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のコードを見てみましょう。

サンプル3(抜粋)
result = base_func1

はい、ココ。
もともと右辺はbase_func1()だったのに、呼び出し演算子()が取れてbase_func1にしれっと変わっていました。
変えた理由は、base_func1()と呼び出し演算子をつけたままだとエラーが起きるからです。

なぜか?トレースしてエラーが起きる原因を探ってみましょう。(画像はあくまでイメージです)
image.png

LESSON6 解決策を考えよう

呼び出し演算子をつけたままにするとエラーが起こる原因は分かりました。次は解決策を考えましょう。

なお、「呼び出し演算子が無しにすれば動くんでしょ?放置でよくね?はダメです。
場当たり的にbase_func1()base_func1に変えたことを「気持ち悪さ」なんてマイルドな(ふざけた)表現をしていますが、本来呼び出し演算子をつけて実行すべき関数base_func1をつけずに実行するのはミスリードひいては事故の原因になります。

結論としては、「deco1関数の中に関数を作成する」ことで解決します。
百聞は一見に如かず。早速コードを確認しましょう。

サンプル4
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) 
サンプル4の結果
★こんにちは:base_func1から出力★

バッチリですね!
関数deco1の中に関数inner_deco1をつけると呼び出し演算子付きで関数base_func1を実行できるようになった理由ですが、こちらもトレースするのが手っ取り早いです。
image.png

これであとは本当に

@deco1 #デコレータを追加!
def base_func1():
#(中略)
@deco1 #デコレータを追加!
def base_func2():
#(中略)
@deco1 #デコレータを追加!
def base_func3():
#(中略)
#以下、base_func30まで@deco1をつけていく…

とすれば終わりですね!
呼び出し演算子()をつけたままbase_func1base_func30を呼び出せるので、デコレータの@をつけまくる以外に既存コードの修正は必要ないですから!

LESSON7 上司「もっとデコろうぜ!」

さて、残りの関数base_func2base_func30にもデコレータ(@deco1)をつけようとしたところ、上司が通りがかりました。

上司「作業はどうだ?お、いいじゃないか」
上司「ほう、デコレータってのを使うとこんなにもスタイリッシュなコードでデコれるんだね」
上司「じゃあ、★の外にさらに顔文字(^o^)もつけちゃおう。イケるっしょ?よろしく!

はい、追加案件がきました。
「その顔文字いらんやろ」と言いたいところですが、社内政治的な理由から反論はNGです

でも安心してください。デコレータは複数使用することができます。
(^o^)という(今にも吹っ飛ばしたい)顔文字をデコレートするためのデコレータを作成し、デコレート対象の関数base_func1に@をつけてあげればいいのです。

新しく作成するデコレータは関数@deco2としましょう。
イメージはこんな感じです!

@deco2 #新規作成するデコレータ。(*^o^*)を出力する
@deco1
def base_func1():
    return "こんにちは:base_func1から出力"

というわけで全貌を早速コードで確認しましょう!

サンプル5
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) 
サンプル5の結果
(*^o^*)★こんにちは:base_func1から出力★(*^o^*)

オッケーですね!(^o^)
念のため処理をトレースしてみましょう。

image.png

LESSON8 引数問題発生!

謎の顔文字をデコることに成功し、今度こそホントに残りの関数base_func2base_func30にもデコレータ(@deco1@deco2)をつける作業が完了しました。

しかし!
動作確認をしたところ関数base_func2base_func30のうち、引数を必要とするものでエラーが発生しました。
たとえば1つの引数を必要とする関数base_func2です。

サンプル6
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) 
サンプル6の結果
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)

image.png

LESSON9 引数問題を解決するために

原因が分かったので解決策を考えましょう。
当然の話ですがすぐに思いつくのは関数innner_deco2と、inner_deco2から呼び出し実行される関数inner_deco1の両方に仮引数を追加することですね。

サンプル7
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) 
サンプル7の結果
(*^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_func1base_func30)の仮引数がいくつであろうと、問題なく動作します。
当然、関数base_func1base_func30の仮引数の記述を変更する必要はありません。

サンプル8
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) 

サンプル8の結果
(*^o^*)★こんにちは:base_func1から出力★(*^o^*)
(*^o^*)★ニーハオ:base_func2から出力★(*^o^*)
(*^o^*)★Helloチャオ:base_func3から出力★(*^o^*)

オッケーです!
無事、デコレート対象の関数(base_func1base_func30)の仮引数が0でも1でも10でもなんでも引数を渡すことができるようになりました!

上司からの与えられたミッションは無事完了です!
お疲れ様でした!

次回予告

あなたは今回の作業を通し、最低限のデコレータを実装することができるようになりました。
ホッとしたのもつかの間、翌日あなたはまたしても上司に呼ばれこう言われました。

上司「申し訳ないんだけど、またちょっとお願いがあってね。」
上司「昨日、(*^o^*)と★でデコる機能つけてもらったじゃん。(*^o^*)はなんかふざけてるみたいだからやっぱり消してもらいたいんだ。」
上司「あと、★なんだけど、今の時代多様性っていうのかな?〇とは▽とかいろんな記号に対応できるようにしたいんだよね!」
上司「てなわけで対応よろしく!君ならできるだろ?あのデコレータって機能でさ!」

繰り返しますが社内政治的な理由から反論はNGです。
あなたは爽やかに「了解です!」と返答し、取り掛かることにしました。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?