皆さんこんにちは。100分の1秒を争うアスリートや、複雑なアルゴリズムを用いて実行速度を計測する競技プログラミングプレイヤーに憧れている小幡です。
今回はプログラムの処理時間を計測するコードを紹介します。アルゴリズムの勉強などをしていると、自分のコードがどのくらいの速度で実行されているのかなど気になり始めると思います。そんな時に処理時間を計測する簡単な方法を知っておくと、すぐに実装できてプログラムの処理時間を知ることが出来て便利です。
紹介する方法は、プログラムの中で開始時刻の値と終了時刻の値を引き算し、標準出力するようになっています。最終目的は「何秒経過したか?」を知る事なので、途中に出てくるtime.time()で取得する時間はエポックの秒数であるということも説明していきます。
環境:Windows10, Python3.9.2
私もこの記事を書くまでは「エポックとはなにか?」、「time.time()を説明すると、どんな言葉になるのか?」という事がわからずに公式リファレンスなどをまじまじと見る事になりました。やはり「教える事で学ぶ」というのは重要な事だと感じています。
time.time()の使用について
さて、今回の処理時間を計測するという目的を達成するために、timeモジュールのtime()を使用します。少し紛らわしいですが、timeモジュールには様々なメソッドが用意されており、その中のtimeというメソッドを使用するということです。他にはlocaltimeやgmtimeなどがあります。
全てのメソッドを確認したい場合は公式リファレンスをどうぞ。
より利便性の高い時刻を取得できるdatetimeモジュールなどを使えば、様々な時刻表現が可能です。例えば「現在時刻は〇時〇分〇秒です」と言うようなデータが取得できたりします、
しかし今回は「何秒経過したか?」を取得することがゴールなので、より覚えやすいtimeモジュールのtimeメソッドを使用します。後程説明しますが、time.time()は、経過時間をfloat型で取得できるため、計算と出力の構造がわかりやすいというメリットもあります。
time.time()の公式リファレンスでの説明は以下です。
エポック からの秒数を浮動小数点数で返します。 エポックの具体的な日付とうるう秒 (leap seconds) の扱いはプラットフォーム依存です。 Windows とほとんどの Unix システムでは、エポックは (UTC で) 1970 年 1 月 1 日 0 時 0 分 0 秒で、うるう秒はエポック秒の時間の勘定には入りません。 これは一般に Unix 時間 と呼ばれています。 与えられたプラットフォームでエポックが何なのかを知るには、 time.gmtime(0) の値を見てください。
https://docs.python.org/ja/3/library/time.html
一読しただけで理解するのは難しいので、とりあえず実際に取得できる値を標準出力してみます。
>>> from time import time
>>> time()
1630056023.131306
まず、Pythonの対話型シェルに入ります。
1行目でtimeモジュールをfromで指定し、timeメソッドをインポートします。timeモジュールはPythonの標準ライブラリなので、インストールは不要です。
2行目でインポートしたtimeメソッドを呼び出します。
3行目にその結果が出力されています。
この数値はfloat型の値で、エポックと呼ばれる環境依存のある時点から経過した秒数を表しています。簡単に説明すると「とある時点からの経過した秒数」であるので、30秒後にまた同じようにtime()を実行すると30が足された数値が取得できるということです。
仮に1回目のtime()が「100.000(秒)」だとしたら、30秒後の2回目のtime()は、「130.000(秒)」となります。
ちなみに、公式リファレンスに記述してある通りですが、エポックがいつを基準に数値を表しているか確認してみます。timeモジュールのgmtime(0)で確認できます。
>>> from time import gmtime
>>> gmtime(0)
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
私の環境でのエポックは、
「1970年1月1日、0時0分0秒、木曜日(tm_wday=0 の場合、月曜日)、366日中(うるう年を含む)の1日目、サマータイムではない」を表しているようです。
ぜひあなたの環境でもエポックをチェックしてみてください!
現在時刻を保持する
ということで、「エポックから経過した秒数を取得する」ことが出来るようになりました。
ここまで読んで勘のいい人ならば、この秒数を使えばプログラムが処理するために「何秒経過したか?」の問題を解決する方法が具体的にわかったかもしれません。
「何秒経過したか?」をもとめるには、「終了時刻の数値」から「開始時刻の数値」を引けば良いのです。
つまり「開始時間」は「エポックから経過した秒数を取得する」ことで保持可能となります。
実際に「開始時間」を作成してみます。ここからはより実際的に1つのファイルに「何かを処理する関数」と「処理時間を計測する関数」を作成します。大枠は以下の様になります。
def main():
elapsed_time = measure_time(calculate_something)
# 計測時間を小数点第三位で四捨五入して標準出力
print('結果')
def measure_time(func):
"""処理時間を計測する関数
"""
func()
def calculate_something():
"""何かを処理する関数
"""
pass
if __name__ == '__main__':
main()
全体を俯瞰してみてみると、「メイン関数」、「処理時間を計測する関数」、「何かを処理する関数」と最後にいつものコマンドラインからのモジュール使用のIF文があることがわかります。
現在時刻を計測開始した時刻で引く
さて、それではいよいよ問題の核心部分である「何秒経過したか?」のコードを書いていきます。もちろん記述場所は、「処理時間を計測する関数」の中です。
コードは以下になります。
def measure_time(func):
"""処理時間を計測する関数
"""
from time import time
# 開始時間を保持
start = time()
# 何かを処理する
func()
# 終了時間を終了時間で引く
elapsed_time = time() - start
return elapsed_time
詳しく見ていきます。
2行目と3行目はdocstring(ドックストリング)と呼ばれる説明文です。コメント文と言えば、’#’も使えますが、このドックストリングを使うと便利な事がいくつかあります。公式リファレンスを覗くとたくさん利用方法が出てきますが、「一定の書き方が存在する」、「ドックストリングは様々な方法で利用価値がある」ということです。あまり利用している人は多くないかもしれませんが、help()関数で呼び出したり、ドックストリング内でテストが行えたりします。
「利用価値」の1つとして、関数やクラスの概要を書いておくことで、そのもののコードを見る事なく全体像を把握することができます。
またVSCodeなどのIDE(総合開発環境)を使用している場合は、Ctrlキーを押しながらマウスオーバーしたりすると、関数の概要が確認できたりします(環境によってことなります)
4行目はtimeモジュールのtimeをインポートしています。これにより、使用するときにいちいちtime.time()とする必要がなくなり、time()だけで、「timeのtimeを呼び出す」と言うことが可能になります。
7行目でtime()で取得した「エポックから経過した秒数」を変数startに代入しています。これにより開始時間を保持することができます。
10行目は、始めてみると少し難しいかもしれませんが、引数として渡された関数の処理を行っています。引数と言えば、整数型や文字列型を思い浮かべる人が多いと思いますが、今回は関数を引数に入れるということです。そして渡された関数をここで処理しているということです。
つまり、「開始時間を保持した後に、計測したい処理を行う」という事が、10行目で行われています。仮に「計測したい処理(func())」が10秒かかったと考えると、次の計測時間算出がわかりやすくなると思います。
13行目ではelapsed_time(計測時間)変数に、time()によって生成された「計測したい処理が終わった後の時間(終了時間)」から「開始時間」を引いた数値を代入しています。
先ほども説明しましたが、「開始時間」が仮に「100,000(秒)」だとして、「計測したい処理(func())」が10秒かかったと考えると、「終了時間」は「110.000(秒)」となりますので、110.000 – 100.000 = 10.000 という計算が出来ることになります。
15行目で計測時間を返却しています。
何かを処理する関数を作る
先ほど引数に関数を渡すと説明しましたが、ここに計測したい関数を渡すことになりますので、その計測したい関数を作成します。
今回は例として、「0から1億と1未満までの整数を全て足す処理」を作ります。コードは以下になります。例題なので詳細説明は割愛しますがFOR文で0から順番に1・2・3・・・1億まで足し算している処理になっていて、最後に足した結果も出力しています。
def calculate_something():
"""何かを処理する関数
"""
print('=============== 処理開始 ===============')
result = 0
end_point = 100000001
for i in range(end_point):
result += i
print(f'0から{end_point}未満までを足すと、{result}')
print('=============== 処理完了 ===============')
差の小数点第三位を四捨五入して出力する
いよいよ最後にメイン関数の説明です。
メイン関数では「処理時間を計測する関数に、計測したい関数を引数として渡して処理」します。
処理結果として計測時間が返却されますので、それを変数に入れます。
その結果を出力すれば「計測時間」を確認できる、という内容です。
私は常々print関数を使用して文字列のフォーマットをするとき、末尾にformat()を付けていました。先日後悔した「Pythonで作るRPG」の企画の中でも全てこれだったわけですが、文字列のフォーマットの方法はたくさんあります。
今回の文字列フォーマットは今まで多様してた形と少し違う形を採用します。
def main():
elapsed_time = measure_time(calculate_something)
# 計測時間を小数点第三位で四捨五入する
print(f'処理時間は、{elapsed_time}秒です')
5行目でprint関数を使用していますが、シングルクォーテーションの前に「f」を入れる事で、formatが可能になります。この「f」を付けた後は、「{変数名}」(波括弧の中に変数名)とすることで、文字列に値を挿入することができるというわけです。
また以下様に旧来のformatも可能ですが、次に説明する四捨五入が簡単に行う方法が使えません。さらに、「f」を使用した方が処理速度が速いらしいので、これからformatについて学ぶ人は新しい方法で慣れておくと良いかもしれません。以下が旧来のフォーマット文です。
print('処理時間は、{}秒です'.format(elapsed_time))
話を元に戻しましょう。出力結果は以下の通りです。
理時間は、8.022904872894287秒です
8秒くらいなのはわかったのですが、小数点以下の位が多すぎて見にくいですね。これはエポックの説明で見てきたとおり、取得する数値がfloat型でとても細かい数値まで取得しているからです。
では、見やすいように小数点第三位を四捨五入してみましょう。実装は簡単です。
先ほど紹介したprint関数の変数を代入している箇所に注目してください。その変数名の後に「:.2f」(コロン+ドット+2f)を追加するだけです。
修正したコードは以下です。
print(f'処理時間は、{elapsed_time:.2f}秒です')
出力結果は以下です。
理時間は、8.02秒です
8.02秒と表示することができました。良い感じですね。
この詳細は、Python, formatで書式変換(0埋め、指数表記、16進数など)を参照すると理解が深まると思います。他にも0埋めや、パーセント表示、丸め込み、などが可能です。
今回重要なのは小数点以下何位を四捨五入するか?ということです。第二位までを表示させたいときは「:.2f」第三位までを表示させたいときは「:.3f」になり、数字の箇所を期待数数値に変える事で実現できます。
以上で、「何秒経過したか?」の結果を取得することができました。
まとめ
結果的に以下のコードが完成します。
def main():
elapsed_time = measure_time(calculate_something)
# 計測時間を小数点第三位で四捨五入する
print(f'処理時間は、{elapsed_time:.2f}秒です')
def measure_time(func):
"""処理時間を計測する関数
"""
from time import time
# 開始時間を保持
start = time()
# 何かを処理する
func()
# 終了時間を終了時間で引く
elapsed_time = time() - start
return elapsed_time
def calculate_something():
"""何かを処理する関数
"""
print('=============== 処理開始 ===============')
result = 0
end_point = 100000001
for i in range(end_point):
result += i
print(f'0から{end_point}未満までを足すと、{result}')
print('=============== 処理完了 ===============')
if __name__ == '__main__':
main()
説明してきたことを順番に並べてみると以下の様になります。
- time.time()を使用できるようにする
- 計測開始時刻を保存する
- 計測終了時刻を取得し、開始時刻で引く
- 差を保存する
- 差の小数点第三位を四捨五入する
- 出力する
以上の流れでプログラムの処理時間を出力することができました。
いかがだったでしょうか?「何秒経過したか?」という結果を知る事は、アルゴリズムの計測だけでなく、テストの計測や、クライアントに明示するために使う事もあるかもしれません。
記事として書いてみて、まだまだ知らないPythonの知識がたくさん見つかり良かったです。
それではまた次回も皆さんにお会いできることを楽しみにしています。