Python3 デコレーターを使ってcron用のスクリプトの出力をうまいこと書こう

こんにちは、kisseです。

前回、Python3 関数オブジェクトを関数に渡せるのを生かして、cron用スクリプトの出力をうまいこと記述するという記事を書いたのですが、
友人に見せたところ”デコレータ使えばもっと簡単に記述できるんじゃない?”っていうコメントをもらったのでデコレータを使って再実装することにしました!



デコレータとは??

前回の記事では、関数に関数を渡すといった方法で実装を行ってましたが、Python3のデコレータはまさにこの記述を簡単にするもののようです。
よくDjangoとか、Flaskの機能とかに、@で始まる名前を自作関数の先頭につけたりとかしていましたが、あれがデコレータと呼ばれるものだったようです。

例えばHTTPレスポンスを作成するような機能をデコレータとしておくことで、ユーザはレスポンスボディを作成する処理のみ記述すれば良いことになります。

まずは簡単に書いてみる

以下にサンプルを示します。

import time
"""
実行時間出力デコレータ
"""
def cronUtil(func):
  def _wrapper(*args, **kwargs):
    print('Start Process.')
    st = time.time()
    ret = func(*args, **kwargs)
    pt = int((time.time() - st) * 1000)
    print('Done, {}ms.'.format(pt))
    return ret
  return _wrapper

@cronUtil
def add(a, b):
  return a + b

if __name__ == '__main__':
  print(add(3, 5))

以下が実行結果です。

Start Process.
Done, 0ms.
8

こんな感じでより簡潔に記述できたような気がします。



デコレータに引数を持たせてみる

デコレータも関数なので、引数を持たせることができます。  
例えば上記の例では、全ての関数において’Start Process’の文字列が出力されますが、この文章を引数で変えられるようにしましょう。

上記の例では、関数を受け取る層と、引数を展開して渡された関数に渡すそうの2つの層があるように見えますが、
今度は、関数ではなくデコレータの方に引数を流す層を作成するイメージです。

先ほどの例にmessageという引数を取れるようにしたのが以下です。

import time

"""
引数付きデコレータ
"""
def cronUtilWithArgs(message):
  def _arg_receiver(func):
    def _wrapper(*args, **kwargs):
      print(message)
      print(message)
      st = time.time()
      ret = func(*args, **kwargs)
      pt = int((time.time() - st) * 1000)
      print('Finished, {}ms'.format(pt))
      return ret
    return _wrapper
  return _arg_receive

@cronUtilWithArgs('Starting...')
def add(a, b):
  return a + b

if __name__ == '__main__':
  print(add(3, 5))

デコレータに引数を渡す際には通常の関数に引数を渡すのと同様に()の中に値を記述します。

実行結果が以下です。
確かに出力内容が少し変わりました。

Starting...
Finished, 0ms
8

キーワード引数つきデコレータ

2個目の例では、毎回毎回メッセージを決めてあげる必要があります。
これでも良いですが、デフォルトのメッセージだけ決めておいて必要なとき以外は楽したいという人はデコレータにキーワード引数を取れるようにしてあげましょう。

以下のように通常の関数と同じようにキーワード引数を設定してあげましょう。

"""
キーワード引数付きデコレータ
"""
def cronUtilWithKeywordArgs(message='Processing'):
  def _arg_receiver(func):
    def _wrapper(*args, **kwargs):

      print(message)
      st = time.time()
      ret = func(*args, **kwargs)
      pt = int((time.time() - st) * 1000)
      print('Finished, {}ms'.format(pt))
      return ret
    return _wrapper
  return _arg_receiver

@cronUtilWithKeywordArgs(message='Starting...')
def add(a, b):
  return a + b

@cronUtilWithKeywordArgs()
def sub(a, b):
  return a - b

if __name__ == '__main__':
  print(add(3, 5))
  print(sub(7, 2))
Starting...
Finished, 0ms
8
Processing
Finished, 0ms
5

このようにデコレータに対してキーワード引数を設定することができました。

キーワード引数つきデコレータの欠点として、デフォルトの引数の値を使う場合でも()をつけてあげないと文法エラーが出てしまう点があげられます。
この点に関して、デフォルトの値を使う場合には()を不要にするように努力をしている方もいらっしゃるので、頑張る方はそちらをお探しください。笑

今回の記事ではそこまで頑張りません。笑



おわり

デコレータを用いると、だいぶ記述量を減らせます。

同じような処理を部分的に何度も繰り返すようなシチュエーションがある場合には、デコレータを利用してみるとだいぶ楽になります。

最後まで読んでいただきありがとうございます!

あわせて読みたい