こんにちは。てぃろです。
今回はPythonで作ったクラスライブラリをデコレータで書き換えるための概要を解説します。
特に、クラスを継承してメソッドをオーバーライドする箇所についてどのように書くといいのか少しだけ解説も入れます。
先に結論を書くと、以下の通りです。
- 書き換えは簡単
- 可読性が上がる = メンテナンスコストが下がる
- 実行速度が上がる = Lambdaのコストが下がる
では、順番に見ていきましょう。
なぜ、デコレータで書き換えたいのか
以前からご紹介しているVueSlsAppという自作のサーバーレスアプリケーションフレームワークがあります。詳細はこちらの記事とGithubを見てください。
これを最初作ったとき、framework.pyというコアな部品ををクラスで作りました。もともとJavaの経験が長いのでオブジェクト指向が染みついていたんですよね。
でも、Pythonを勉強すればするほどデコレータで書けたほうがかっこいい!と思うようになりました(笑)
そんな単純な理由で書き換えを決意しました。
デコレータの書き換えにメリットはあるのか?
ただ、そこは実用的なフレームワークを目指しているので以下も気になります。
- 書き換えに工数はかかりそうか?
- 可読性は悪くならないか?
- 実行速度に違いはあるか?
これらを検証するにもまずは簡単なサンプルを書き換えることで検証してみました。
サンプルコードは以下のGithubに上げてあります。ここからの解説はこれを見ながら読んでいただくとよいかと思います。
書き換えの工数は大きくないし、読み易い
サンプルを見ていただくとわかりますが、書き換え個所はざっくり以下の4点程度です。
- クラスにて、classの宣言部をdecoratorの宣言部に変更する
- クラスにて、オーバーライドの対象となるメソッドを消す
- メインにて、クラスの継承を単純なメソッドの定義に変更
- メインにて、インスタンス化は不要で直接メソッドを呼び出す
コードと合わせて見ていきます。まず継承元であるクラスの書き換えです。
これを、
#!/usr/bin/env python
# -*- coding: utf-8 -*-
class SampleClass(object):
def __init__(self):
pass
def controller(self, query_strings=None, body=None, path_params=None):
print("controller is not implemented.")
def handler(self, num_loop=10000):
query_strings = 'Query'
body = 'Body'
path_params = 'Path'
for i in range(0, num_loop + 1):
self.controller(query_strings, body, path_params)
return True
これに書き換えました。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def sample_decorator(controller): # Classの宣言部を変更
# controllerのインターフェースとなるメソッドがいらない
def handler(num_loop=10000):
query_strings = 'Query'
body = 'Body'
path_params = 'Path'
for i in range(0, num_loop + 1):
controller(query_strings, body, path_params)
return True
return handler
handler()
の中身はほぼ変わっていません。変わったことといえば、handler()
内のcontroller()
の呼び出し時にself.
が消えたくらいです。
次に、呼び出す側を見ていきます。
これを、
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
from sample_class import SampleClass
class ImplementedClass(SampleClass):
def controller(self, query_strings=None, body=None, path_params=None):
print('This is NEW controller !!!')
if __name__ == '__main__':
start = time.perf_counter()
sc = ImplementedClass()
result = sc.handler()
print(result)
elapsed_time = time.perf_counter() - start
print('elapsed_time: %f [sec]' % elapsed_time)
このように書き換えることができます。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
from sample_decorator import sample_decorator
@sample_decorator # デコレータの宣言だけでクラスの宣言はいらない
def controller(query_strings=None, body=None, path_params=None):
print('This is NEW controller !!!')
if __name__ == '__main__':
start = time.perf_counter()
result = controller() # クラスのインスタンス化が不要!
print(result)
elapsed_time = time.perf_counter() - start
print('elapsed_time: %f [sec]' % elapsed_time)
これも非常に書き換え個所は少なかったです。数行の違いではありますが、圧倒的にシンプルでかっこよくなりました!しかも読み易い。
クラスのロジックについてもほぼ手を入れずに変換できるので、機能的にも問題ないと言い切れます。
結論:書き換え工数は大きくなく、可読性も問題ありません。
実行速度はほぼ変わらないけど、運用コストが下がるかも?
サンプルコードは実行速度を継続できるように作っていますので、これをそのまま実行して実行時間を計測していきます。
実行環境は私のローカルマシンを使います。Visual Studio Code上から開いたコマンドプロンプトを使い、単純にpythonコマンドで実行します。
ループは10000、試行はそれぞれ10回行いました。結果は以下です。時間の単位は、secです。
デコレータ | クラス | |
1 | 1.710336 | 1.754681 |
2 | 1.74291 | 1.716729 |
3 | 1.726866 | 1.749106 |
4 | 1.762045 | 1.684454 |
5 | 1.726799 | 1.724049 |
6 | 1.667672 | 1.721026 |
7 | 1.736057 | 1.715892 |
8 | 1.68565 | 1.75286 |
9 | 1.738636 | 1.733255 |
10 | 1.705089 | 1.72037 |
ave | 1.720206 | 1.7272422 |
結果、10回程度の試行でほぼ違いはありませんでした。
これらの実装の違いは、最初のクラスをインスタンス化するか、しないかというところですが、今回の形では実行時間に差は出ませんでした。
クラスのインスタンス化を何度もする、とかのケースの違いでは差が出るかもしれませんが、今回の用途では実行時間の体感上の差はなくどちらでもいいと言えます。
しかし、ことLambdaでどうさせるとなると、0.007secの実行時間の差は大きな差を生みます。
実は、Lambdaの料金体系は実行時間1ms単位なのです。
塵も積もれば山となる。
APIのレスポンスタイムが1msでも早いに越したことはないというだけでなく、運用コストが大きく安くなっていく可能性があるのです。
結論:実行速度が上がる可能性がある = 運用コストが下がる可能性がある
まとめ
今回はクラスの継承とオーバーライドを使う部分をデコレータで書き直してみました。
ここまでの検証の結果は以下の通りです。
- 書き換えは簡単
- 可読性が上がる = メンテナンスコストが下がる
- 実行速度が上がる = Lambdaのコストが下がる
もはや書き換えることはメリットしかないように見えますね!
これから、framework.pyはデコレータで書き直していきたいと思います。
注:実行速度の実験は、あくまで筆者のローカル環境における簡易な実験の結果であるため、誤差の可能性は否定できず、これによってLambdaのコストが下がることを保証するものではありません