Python デコレーターの使い方

Python学習【365日チャレンジ!】56日目のマスターU(@Udemy11)です。

最近、懸垂をして大胸筋を鍛えているのですが、ようやく10回続けてできるようになってきました。

はじめのうちは、3回位が限界だったのですが、やっぱり続けるのってすごいですね。

もともと若い頃にスポーツをしていたので、最初から10回を3セットとかやろうと思っていたのですが、あまりの筋力の落ち具合にびっくりしてしまいました。

懸垂もしっかりと継続して、大胸筋を鍛えたいと思います。

それでは、今日もPython学習をすすめましょう。

昨日の復習

昨日は、クロージャーを学習しました。
関数の中に関数を定義して、実行する一歩手前の関数オブジェクトがクロージャーということですが、外側の関数で定義されたものをいくつか準備した上で、実行するときに準備したものを活用するというような使い方ができました。

def circle_func(pi):
    def rad_func(radius):
        return pi * radius ** 2
    return rad_func
 	
c1 = circle_func(3.14)
c2 = circle_func(3.141592535)
 
c3 = c1(10)
c4 = c2(10)
 
print('半径10、πが3.14の円の面積は、', c3)
print('半径10、πが3.141592535の円の面積は、', c4)

出力結果

半径10、πが3.14の円の面積は、 314.0
半径10、πが3.141592535の円の面積は、 314.1592535

アウター関数の引数に円周率のパイを2パターン代入して、あとは円の半径radiusをインナー関数の引数に代入すれば、円の面積が計算されるクロージャーrad_funcを使って、半径10を代入する2つのパターンを出力しています。

クロージャーは、

外側の変数を記憶した関数オブジェクト

ですので、上記のコードの場合、外側の変数piを記憶して、実行時に呼び出して活用しています。

関数内関数との違いは、アウター関数でインナー関数を実行した返り値ではなく、実行できる関数オブジェクトを返すところでしたね。

それでは、デコレーション!?じゃなかった、デコレーターを学習しましょう!

デコレーター

デコレーターというくらいなので、デコレーションするんだろうな〜となんとなく考えています。
ちなみに、デコレーションの意味を調べてみると、【装飾、飾り】という答えが見つかりました。

Pythonのデコレーターもこの飾り付けに使うものという認識で大丈夫かと思いますが、関数の実行時の前後で他の処理を付け加えたいときに使うものということです。

よくわからないですよね。

私も最初は一体ぜんたいどういうことなのか、全く理解できなかったので、レクチャーを20回は繰り返して見てしまいました。

今でもきちんと理解できているのか不安ですが、頭の中で理解しようとするより、実際にコードを書いたほうが理解できるので、私なりに理解した内容を共有していきます。

関数の実行前後に何かをさせるということなので、イメージとしては次のような感じです。

def add_num(a, b):
    return a + b
 
print('start')
r = add_num(1, 2)
print('end')
 
print(r)

出力結果

start
end
3

変数aとbを加算する関数add_numを用意して、関数実行の前後にstartendを出力しています。

実行結果の出力前後ではなく、実行前後なので、実行結果の3は最後にprint>出力されています。

この関数add_numをインナー関数にして、デコレーターを作ります。

もうこの時点でわけが分からなくなっちゃいますよね。もちろん私もわけがわかっていませんでしたのでご安心を。

デコレートする

先程の関数add_numをデコレートしてみます。

def print_info(func):
    def wrapper(*args):
        print('Start')
        result = func(*args)
        print('End')
        return result
    return wrapper
 
def add_num(a, b):
    return a + b
 
p = print_info(add_num)
r = p(10, 20)
print(r)

出力結果

Start
End
30

ちょっとややこしいのですが、このコードの流れをみてみましょう。

アウター関数print_infofuncという引数をとる関数print_infoの返り値をインナー関数wrapperの関数オブジェクトにして、引数をいくつでもタプル化できる*argsにした関数wrapperwrapperでは、アウター関数の引数名func関数を変数resultにいれて、resultを返り値にして、その前後にStartEndを出力します。

この関数print_infoがデコレーターになるわけですが、よくわからないですよね。

この関数のあとに、デコレートされる関数add_numが定義されていますが、これは変数aとbを足し算する関数です。

そして、実際の実行は、12行目のprint_info(add_num)で実行され、引数funcadd_numが代入されて、クロージャーwrapperが返り値として返されます。

返り値wrapperpに代入されたのち、次の行のp(10, 20)が実行されて、デコレータの中のStartが出力された後、func(*args)つまりadd_num(10, 20)が実行されて変数resultに30が代入された後、Endが出力されます。
ここまでの動作がデコレーターである関数print_infoの実行プロセスです。

その後、返り値であるresultの値(30)がrに代入されて、最終行で出力されるという流れです。

一度や二度この流れを確認しただけでは、理解できないと思います。

私自身、こうして流れを書き出しながら、改めて理解を深められたくらいですので、実際のコードを書きながら理解するしかありません。

@をつけて関数名を記述

実際にデコレーターを使うときは、デコレートする関数の前に@をつけて記述します。

def print_info(func):
    def wrapper(*args):
        print('Start')
        result = func(*args)
        print('End')
        return result
    return wrapper
 
@print_info
def add_num(a, b):
    return a + b
 
r = add_num(10, 20)
print(r)

出力結果

Start
End
30

デコレーターは、Pythonの機能に標準で備わっているものなので、それだけ凡庸性の高い機能であると言えますが、今の時点ではどのような使い方ができるかは全く想像がつきません。

デコレーターは何が便利?

出力結果は同じなので、こんな複雑なコードを書く必要があるのかと疑問に思っちゃいませんか?

私もそう思います。

それではなぜこんな面倒なコードを書くのか?

それは、使い回しができるからです。

関数の実行前後にprint出力する程度なら問題はありませんが、前後に何度も活用したい複雑な処理をさせたい場合などに使い回せるとかなりパフォーマンスが上がります。

今回書いたコードの場合は、新しい引き算をする関数や掛け算をする関数を定義して同じ前後の処理をしたいときは、新しい関数の前の行に@print_infoとつけるだけでできるので、前後の処理をコードにする必要がなくなります。

なかなか理解するのは難しいと思いますが、今の段階では、なんとなくこんな感じということを理解する程度で大丈夫です。

デコレータのネスト

次に、デコレーターの中にデコレーターを入れる処理をしてみます。
先程のprint_infoの中に別のprint出力を加えたデコレータmore_infoを作ってみました。

def print_info(func):
    def wrapper(*args):
        print('Start')・・・・・①
        result = func(*args)
        print('End')・・・・・①'
        return result
    return wrapper
 
def more_info(func):
    def wrapper(*args):
        print('func:', func.__name__)・・・・・②
        print('args:', args)・・・・・②
        result = func(*args)
        print('result:', result)・・・・・②'
        return result
    return wrapper
 
@more_info
@print_info
def add_num(a, b):
    return a + b
 
r = add_num(10, 20)
print(r)

出力結果

func:wrapper
args:(10, 20)
Start
End
result:30
30

@more_infoの次の行に@print_infoを入れて、その下の行にデコレートされる関数を記述しています。

出力結果は次のような感じでデコレートされています。

外のデコレーター②func:wrapper
外のデコレーター②args:(10, 20)
中のデコレータ①Start
実行関数(実行だけで出力されていない)
中のデコレーター①’End
外のデコレーター②’result:30
実行結果30

外側のデコレーターが内部のデコレーターを包み込んで、中の関数をさらに包み込んでいるような感じです。

デコレーターは上の行にかかれているものが下の行のものを包み込むので、順番が入れ替わると結果も入れ替わるので、注意しておきましょう。

実際の使いみちを考えてみた

外側からサンドイッチするようなイメージだったので、実際にそんな構造になっているものを考えたところ、HTMLのコードがタグで挟まれているので、次のようなコードを考えてみました。

def deco_html(func):
    def wrapper(*args):
        result = '<html>' + str(func(*args)) + '</html>'
        return result
    return wrapper
 
def deco_body(func):
    def wrapper(*args):
        result = '<body>' + str(func(*args)) + '</body>'
        return result
    return wrapper
 
@deco_html
@deco_body
def main(text):
    return text
 
s = main('hello')
print(s)

出力結果

<html><body>hello</body></html>

main(text)deco_htmldeco_bodyでデコレートしています。

これまでやってきたように、実行関数の前後にprint出力するだけだとHTMLタグの外側にテキストを出力してしまったので、HTMLタグとBODYタグをstr()で文字列化したmain('hello')の前後に加えてresultに代入しました。

結果、うまい具合に2つのデコレーターに包まれた実行結果を得ることができました。

試行錯誤がポイント

最後のHTMLタグでデコレートする方法は、いろいろと失敗しながらようやく思った出力ができました。

func(*args)NoneTypeだと怒られたり、最初に入れいた**kwargsが不要だと気づいたり、なかなかうまく出力することができませんでした。

エラーが起こるとネットで調べて試してはエラーだったりを繰り返して、なんとか求めていた出力結果を得ることができました。

今回はかなりたくさんコードを書いては消してを繰り返したので、ある程度デコレーターを理解することができたような気がします。

ほんと考えたことが形になると嬉しくなっちゃいますよね。

といったところで、明日もしっかりとPython学習を続けたいと思います。

それでは明日も、Good Python!