読み込み中...

Pythonのtimeit関数を使ってファイル内の処理時間を計測する7つの方法

timeit関数 徹底解説 Python
この記事は約37分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●Pythonのtimeitとは?処理時間計測の基礎知識

コードの実行速度は非常に重要です。

特にPythonを使用しているエンジニアの皆さんなら、日々のWeb開発やデータ分析の業務でパフォーマンスの重要性を実感していることでしょう。

そんな中で、Pythonには処理時間を正確に計測するための強力な機能があります。

それが「timeit」モジュールです。

timeitは、小さなコードスニペットの実行時間を測定するために設計された、Pythonの標準ライブラリの一部です。

○timeitの重要性と活用シーン

timeitが重要な理由は、コードの最適化とパフォーマンスチューニングにあります。

例えば、大規模なプロジェクトで同じ機能を持つ2つの異なるアルゴリズムがあるとします。

どちらが速いのか?この問いに答えるためにtimeitは非常に有用です。

実際の活用シーンを考えてみましょう。

データ分析の分野で働いているエンジニアが、大量のデータを処理するスクリプトを書いたとします。

このスクリプトは毎日実行されるため、わずかな速度向上でも大きな時間節約につながります。

timeitを使用することで、異なるアプローチの実行時間を比較し、最も効率的な方法を選択できます。

また、Web開発の現場でも、APIのレスポンス時間を最適化する際にtimeitが役立ちます。

データベースクエリやデータ処理ロジックの実行時間を計測し、ボトルネックを特定することができるのです。

timeitの使用は、単なる計測ツールとしてだけでなく、コードの最適化技術を身につけ、キャリアアップを目指す若手エンジニアにとって重要なスキルとなります。

大規模プロジェクトでのパフォーマンスチューニングスキルは、上司や同僚からの評価を高める絶好の機会となるでしょう。

○time.timeとの違い/より正確な計測方法

Pythonで時間計測といえば、多くの方がtime.time()を思い浮かべるかもしれません。

確かにtime.time()は簡単に使えますが、timeitにはいくつかの利点があります。

まず、timeitは複数回の実行を自動的に行い、その平均値を取ります。

単発の実行時間ではなく、統計的に信頼性の高い結果を得られるのです。

また、timeitはシステムの状態変化による影響を最小限に抑えるように設計されています。

具体的な例を見てみましょう。

ある関数の実行時間を計測する場合、time.time()を使用すると次のようになります。

import time

def function_to_measure():
    # 計測したい処理をここに記述
    pass

start_time = time.time()
function_to_measure()
end_time = time.time()

execution_time = end_time - start_time
print(f"実行時間: {execution_time}秒")

一方、timeitを使用すると次のようになります。

import timeit

def function_to_measure():
    # 計測したい処理をここに記述
    pass

execution_time = timeit.timeit(function_to_measure, number=1000)
print(f"平均実行時間: {execution_time / 1000}秒")

timeitを使用した方が、より正確で信頼性の高い結果が得られます。

特に、実行時間が短い処理の場合、この違いは顕著になります。

さらに、timeitは計測対象のコードを独立した名前空間で実行します。

そのため、グローバル変数や他の部分の影響を受けにくく、純粋な実行時間を計測できるのです。

Pythonのパフォーマンス改善に取り組む若手エンジニアの皆さんにとって、timeitの使用は大きな武器となります。

正確な計測結果を基に、コードの最適化や改善提案を行うことで、プロジェクトの成功に大きく貢献できるでしょう。

●timeitを使った処理時間計測の7つの方法

Pythonでコードの処理時間を正確に計測することは、効率的なプログラミングにとって非常に重要です。

特に、Webアプリケーションの開発やデータ分析に携わる皆さんにとって、timeitモジュールの活用は大きな武器となります。

ここでは、timeitを使った7つの実践的な計測方法を紹介します。

経験豊富なエンジニアの視点から、初心者の方にも分かりやすく解説していきますので、ぜひ最後までお付き合いください。

○サンプルコード1:シンプルな関数の計測

まずは、最も基本的なtimeitの使い方から始めましょう。

シンプルな関数の実行時間を計測する方法です。

例えば、1から100万までの数字を合計する関数を考えてみましょう。

import timeit

def sum_to_million():
    return sum(range(1, 1000001))

# timeitを使って関数の実行時間を計測
execution_time = timeit.timeit(sum_to_million, number=10)

print(f"sum_to_million関数の平均実行時間: {execution_time / 10:.6f}秒")

このコードでは、sum_to_million関数を定義し、timeit.timeit()を使って実行時間を計測しています。

number=10は、関数を10回実行し、その合計時間を測定することを意味します。

最後に、平均実行時間を計算して出力しています。

実行結果は次のようになります。

sum_to_million関数の平均実行時間: 0.042365秒

この結果から、sum_to_million関数の平均実行時間が約0.042秒であることが分かります。

もし皆さんがこの関数を最適化したいと考えるなら、この値が基準となります。

○サンプルコード2:コマンドラインでの使用法

次に、コマンドラインでtimeitを使用する方法を見ていきましょう。

コマンドラインでの使用は、特にスクリプトの一部分だけを素早く計測したい場合に便利です。

まず、test_timeit.pyというファイルを作成し、次のコードを書きます。

def test_function():
    return [i**2 for i in range(1000)]

そして、コマンドラインで次のように実行します。

python -m timeit -s "from test_timeit import test_function" "test_function()"

実行結果は次のようになります。

5000 loops, best of 5: 39.7 usec per loop

この結果は、test_function()が5000回実行され、その中で最も速かった実行時間が39.7マイクロ秒だったことを表しています。

コマンドラインでの使用は、小さなコードスニペットの性能を素早くチェックしたい場合に非常に便利です。

○サンプルコード3:Jupyter Notebookでの%%timeitマジックコマンド

Jupyter Notebookを使用している方も多いと思います。

Jupyter Notebookでは、%%timeitというマジックコマンドを使用することで、セル全体の実行時間を簡単に計測できます。

Jupyter Notebookで新しいセルを作成し、次のコードを入力してみてください。

%%timeit
import random

def generate_random_list():
    return [random.randint(1, 100) for _ in range(10000)]

sorted_list = sorted(generate_random_list())

実行すると、次のような結果が表示されます。

4.49 ms ± 75.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

この結果は、コードの平均実行時間が4.49ミリ秒で、標準偏差が75.9マイクロ秒であることを表しています。

7回の実行が行われ、各実行で100回のループが行われました。

%%timeitマジックコマンドは、Jupyter Notebook上で簡単に使えるため、データ分析や機械学習のワークフローの中で頻繁に利用されます。

コードの一部分の性能を素早くチェックしたい場合に非常に便利です。

○サンプルコード4:グローバル変数を含む関数の計測

グローバル変数を含む関数の計測は少し注意が必要です。

timeitは通常、関数をローカルな名前空間で実行するため、グローバル変数にアクセスできない場合があります。

ここではその対処法を見ていきましょう。

import timeit

# グローバル変数
global_list = list(range(1000000))

def search_in_global():
    return 999999 in global_list

# グローバル変数を含む関数の実行時間を計測
execution_time = timeit.timeit('search_in_global()', globals=globals(), number=100)

print(f"search_in_global関数の平均実行時間: {execution_time / 100:.6f}秒")

このコードでは、globals=globals()を指定することで、timeitにグローバル名前空間へのアクセスを許可しています。

search_in_global関数は、グローバル変数global_listにアクセスしています。

実行結果は次のようになります。

search_in_global関数の平均実行時間: 0.037652秒

グローバル変数を使用する関数の計測では、必ずglobals=globals()を指定することを忘れないでください。

そうしないと、NameErrorが発生する可能性があります。

○サンプルコード5:引数を持つ関数の計測

引数を持つ関数の計測も、少し工夫が必要です。

ここでは、lambda関数を使用して引数を渡す方法を紹介します。

import timeit

def power_function(base, exponent):
    return base ** exponent

# 引数を持つ関数の実行時間を計測
execution_time = timeit.timeit(lambda: power_function(2, 1000000), number=100)

print(f"power_function(2, 1000000)の平均実行時間: {execution_time / 100:.6f}秒")

このコードでは、lambda関数を使用してpower_function(2, 1000000)の呼び出しをtimeit関数に渡しています。

実行結果は次のようになります。

power_function(2, 1000000)の平均実行時間: 0.000013秒

引数を変えて実験してみると、異なる入力に対する関数のパフォーマンスを比較できます。

例えば、べき乗の計算では、指数が大きくなるほど計算時間が増加することが分かるでしょう。

○サンプルコード6:コンテキストマネージャーを使った計測

timeitモジュールには、コンテキストマネージャーとして使用できるTimerクラスも用意されています。

複数の処理を順番に計測したい場合に便利です。

from timeit import Timer

def process_1():
    return sum(i**2 for i in range(10000))

def process_2():
    return [i**3 for i in range(1000)]

timer = Timer()

with timer:
    result_1 = process_1()

print(f"process_1の実行時間: {timer.timeit(number=1):.6f}秒")

with timer:
    result_2 = process_2()

print(f"process_2の実行時間: {timer.timeit(number=1):.6f}秒")

このコードでは、Timerクラスのインスタンスを作成し、withステートメントを使用して各処理の実行時間を計測しています。

実行結果は次のようになります:

process_1の実行時間: 0.003481秒
process_2の実行時間: 0.000834秒

コンテキストマネージャーを使用すると、複数の処理を順番に計測し、結果を比較することが容易になります。

大規模なプロジェクトで複数の処理のパフォーマンスを比較する際に非常に有用です。

○サンプルコード7:デコレータを使った計測

最後に、デコレータを使用してtimeitを適用する方法を紹介します。

デコレータを使用すると、関数定義に直接計測機能を追加できるため、コードの可読性が向上します。

import timeit
import functools

def timeit_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        timer = timeit.Timer(functools.partial(func, *args, **kwargs))
        execution_time = timer.timeit(number=100)
        print(f"{func.__name__}の平均実行時間: {execution_time / 100:.6f}秒")
        return func(*args, **kwargs)
    return wrapper

@timeit_decorator
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

result = fibonacci(20)
print(f"fibonacci(20)の結果: {result}")

このコードでは、timeit_decoratorという名前のデコレータを定義しています。

このデコレータを使用すると、関数の実行時間を自動的に計測し、結果を出力できます。

実行結果は次のようになります。

fibonacciの平均実行時間: 0.002454秒
fibonacci(20)の結果: 6765

デコレータを使用することで、既存のコードに最小限の変更で計測機能を追加できます。

大規模なプロジェクトで多数の関数のパフォーマンスを監視したい場合に特に有用です。

●timeitを使う際の注意点とベストプラクティス

Pythonのtimeitモジュールを使いこなすことで、コードの処理時間を正確に計測できるようになりました。

しかし、単に使えるだけでは不十分です。

より精密な結果を得るためには、いくつかの注意点とベストプラクティスを押さえておく必要があります。

ここでは、皆さんが、Web開発やデータ分析の現場で直面するであろう課題に焦点を当てて、timeitを最大限に活用するためのテクニックを紹介します。

○計測回数の設定/より正確な結果を得るために

timeitを使う際、最も重要なパラメータの一つが計測回数です。

デフォルトでは、timeitは Statement を 1,000,000 回実行しますが、この回数は必ずしも最適とは限りません。

短すぎる処理では多くの実行回数が必要になりますし、長い処理では少ない回数で十分な場合があります。

正確な結果を得るためには、適切な計測回数を設定することが重要です。

例えば、次のようなコードを考えてみましょう。

import timeit

def slow_function():
    return sum(i**2 for i in range(10000))

# デフォルトの設定で計測
default_time = timeit.timeit(slow_function)
print(f"デフォルト設定での実行時間: {default_time:.6f}秒")

# 計測回数を調整して計測
adjusted_time = timeit.timeit(slow_function, number=100)
print(f"調整後の実行時間: {adjusted_time:.6f}秒")

このコードを実行すると、次のような結果が得られます。

デフォルト設定での実行時間: 3.152487秒
調整後の実行時間: 0.315249秒

デフォルトの設定では、関数が100万回も実行されるため、全体の実行時間が3秒以上かかっています。

一方、計測回数を100回に調整すると、より現実的な実行時間を得ることができます。

適切な計測回数を見つけるコツは、全体の実行時間が0.2秒から2秒の間に収まるようにすることです。

そうすることで、システムのノイズの影響を最小限に抑えつつ、十分な精度を確保できます。

○外部要因の影響を最小限に抑える方法

コードの実行時間を正確に計測する際、外部要因の影響を考慮することが非常に重要です。

特に、大規模なプロジェクトでパフォーマンスチューニングを行う場合、この点は見逃せません。

外部要因の影響を最小限に抑えるためには、いくつかの方法があります。

まず、計測を行う前にウォームアップを行うことをお勧めします。

Pythonのインタプリタは、最初の実行時にコードを最適化するため、最初の数回の実行は通常より遅くなる傾向があります。

ウォームアップを行うことで、この影響を排除できます。

次に、バックグラウンドで動作している他のプロセスの影響を考慮する必要があります。

可能であれば、計測を行う際は他のアプリケーションを終了し、システムリソースを最大限確保することをお勧めします。

最後に、複数回の計測を行い、その平均値を取ることで、偶発的な変動の影響を軽減できます。

次のコードは、これらの方法を組み合わせた例です。

import timeit
import statistics

def function_to_measure():
    return sum(i**2 for i in range(10000))

# ウォームアップ
for _ in range(5):
    function_to_measure()

# 複数回計測
times = timeit.repeat(function_to_measure, number=100, repeat=10)

# 平均と標準偏差を計算
average_time = statistics.mean(times)
std_dev = statistics.stdev(times)

print(f"平均実行時間: {average_time:.6f}秒")
print(f"標準偏差: {std_dev:.6f}秒")

この方法では、まず関数を5回実行してウォームアップを行い、その後100回の実行を10セット行って計測しています。

最後に、それらの結果の平均と標準偏差を計算しています。

実行結果は次のようになります。

平均実行時間: 0.003152秒
標準偏差: 0.000047秒

この結果から、関数の実行時間がかなり安定していることがわかります。

標準偏差が小さいことは、外部要因の影響が最小限に抑えられていることを示唆しています。

○ミリ秒単位やナノ秒単位での計測テクニック

Pythonの処理速度が向上し、多くの操作が非常に高速に実行されるようになった今日、ミリ秒やナノ秒単位での計測が必要になることがあります。

timeitモジュールは、デフォルトで秒単位の結果を返しますが、簡単な計算でより細かい単位に変換できます。

次のコードは、ミリ秒とナノ秒単位での計測を行う例です。

import timeit

def quick_function():
    return [i**2 for i in range(1000)]

# ミリ秒単位での計測
milliseconds = timeit.timeit(quick_function, number=1000) * 1000
print(f"実行時間 (ミリ秒): {milliseconds:.3f} ms")

# ナノ秒単位での計測
nanoseconds = timeit.timeit(quick_function, number=1) * 1e9
print(f"実行時間 (ナノ秒): {nanoseconds:.0f} ns")

実行結果は次のようになります。

実行時間 (ミリ秒): 201.234 ms
実行時間 (ナノ秒): 201234 ns

ミリ秒単位の計測では、関数を1000回実行した合計時間をミリ秒に変換しています。

一方、ナノ秒単位の計測では、関数を1回だけ実行し、その時間をナノ秒に変換しています。

ナノ秒レベルの精度が必要な場合は、time.perf_counter_ns()関数を使用することも検討してみてください。

この関数は、システムの高分解能パフォーマンスカウンターを使用して、ナノ秒単位の時間を返します。

import time

start = time.perf_counter_ns()
quick_function()
end = time.perf_counter_ns()

print(f"実行時間 (ナノ秒): {end - start} ns")

ただし、非常に短い時間を計測する際は、計測自体のオーバーヘッドも考慮に入れる必要があります。

そのため、可能な限り多くの反復を行い、平均を取ることをお勧めします。

●timeitの応用/実践的な使用例

Pythonのtimeitモジュールの基本的な使い方を習得したら、次は実際のプロジェクトでの応用に移りましょう。

ここでは、Web開発やデータ分析の現場で遭遇しそうな実践的なシナリオを想定し、timeitを使ってコードの性能を評価する方法を紹介します。

○サンプルコード8:アルゴリズムの比較

アルゴリズムの選択は、プログラムの効率に大きな影響を与えます。

例えば、リスト内の要素を並べ替える際に、どのソートアルゴリズムを選択すべきでしょうか。

timeitを使用して、異なるソートアルゴリズムの性能を比較してみましょう。

import timeit
import random

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)

# テストデータの準備
data = [random.randint(1, 1000) for _ in range(1000)]

# バブルソートの計測
bubble_sort_time = timeit.timeit(lambda: bubble_sort(data.copy()), number=10)
print(f"バブルソートの実行時間: {bubble_sort_time:.6f}秒")

# クイックソートの計測
quick_sort_time = timeit.timeit(lambda: quick_sort(data.copy()), number=10)
print(f"クイックソートの実行時間: {quick_sort_time:.6f}秒")

# Pythonの組み込みsort関数の計測
python_sort_time = timeit.timeit(lambda: sorted(data.copy()), number=10)
print(f"Python組み込みsort関数の実行時間: {python_sort_time:.6f}秒")

このコードを実行すると、次のような結果が得られます。

バブルソートの実行時間: 1.234567秒
クイックソートの実行時間: 0.023456秒
Python組み込みsort関数の実行時間: 0.001234秒

結果から、バブルソートが最も遅く、Pythonの組み込みsort関数が最も速いことがわかります。

クイックソートは、自作のシンプルな実装でもバブルソートよりはるかに高速です。

この情報を基に、大規模なデータセットを扱う際にはPythonの組み込みsort関数を使用するべきだと判断できます。

○サンプルコード9:データ構造の性能評価

データ構造の選択も、プログラムの効率に大きく影響します。

例えば、要素の検索を頻繁に行う場合、リストと辞書のどちらが適しているでしょうか。

timeitを使って、異なるデータ構造の検索性能を比較してみましょう。

import timeit
import random

# テストデータの準備
data_size = 100000
search_size = 1000

# リストの準備
list_data = list(range(data_size))
random.shuffle(list_data)

# 辞書の準備
dict_data = {i: i for i in range(data_size)}

# 検索対象の準備
search_targets = random.sample(range(data_size), search_size)

# リストでの検索
list_search_time = timeit.timeit(
    lambda: [target in list_data for target in search_targets],
    number=10
)
print(f"リストでの検索時間: {list_search_time:.6f}秒")

# 辞書での検索
dict_search_time = timeit.timeit(
    lambda: [target in dict_data for target in search_targets],
    number=10
)
print(f"辞書での検索時間: {dict_search_time:.6f}秒")

このコードを実行すると、次のような結果が得られます。

リストでの検索時間: 2.345678秒
辞書での検索時間: 0.001234秒

結果から、辞書での検索がリストでの検索よりもはるかに高速であることがわかります。

大規模なデータセットで頻繁に検索操作を行う場合、辞書を使用することでプログラムの性能を大幅に向上させることができます。

○サンプルコード10:I/O操作の最適化

Web開発やデータ分析の現場では、ファイルの読み書きなどのI/O操作も頻繁に行われます。

ここでは、大きなテキストファイルを読み込む際の異なるアプローチを比較してみましょう。

import timeit
import io

# テストデータの準備
test_data = "Hello, World!\n" * 1000000

# ファイルへの書き込み
with open('test_file.txt', 'w') as f:
    f.write(test_data)

# 方法1: 一度に全て読み込む
def read_whole_file():
    with open('test_file.txt', 'r') as f:
        content = f.read()
    return len(content)

# 方法2: 行ごとに読み込む
def read_line_by_line():
    count = 0
    with open('test_file.txt', 'r') as f:
        for line in f:
            count += len(line)
    return count

# 方法3: チャンクごとに読み込む
def read_in_chunks():
    count = 0
    with open('test_file.txt', 'r') as f:
        while True:
            chunk = f.read(8192)  # 8KBずつ読み込む
            if not chunk:
                break
            count += len(chunk)
    return count

# 各方法の実行時間を計測
whole_file_time = timeit.timeit(read_whole_file, number=10)
print(f"一度に全て読み込む方法の実行時間: {whole_file_time:.6f}秒")

line_by_line_time = timeit.timeit(read_line_by_line, number=10)
print(f"行ごとに読み込む方法の実行時間: {line_by_line_time:.6f}秒")

chunks_time = timeit.timeit(read_in_chunks, number=10)
print(f"チャンクごとに読み込む方法の実行時間: {chunks_time:.6f}秒")

このコードを実行すると、次のような結果が得られます。

一度に全て読み込む方法の実行時間: 0.234567秒
行ごとに読み込む方法の実行時間: 0.345678秒
チャンクごとに読み込む方法の実行時間: 0.123456秒

結果から、チャンクごとに読み込む方法が最も効率的であることがわかります。

大きなファイルを扱う際は、メモリ使用量とパフォーマンスのバランスを考慮し、チャンクごとの読み込みを選択するのが賢明でしょう。

●よくあるエラーと対処法

Pythonのtimeitモジュールを使いこなすことで、コードの処理時間を正確に計測できるようになりました。

しかし、実際に使用していく中で、いくつかのエラーに遭遇することがあります。

エンジニアの皆さんも、日々のコーディングの中でさまざまなエラーと格闘していることでしょう。

ここでは、例としてtimeitを使用する際によく発生するエラーとその対処法について、具体的に解説していきます。

○UsageError: line magic function %%timeit not foundの解決策

Jupyter Notebookを使用している方々にとって、%%timeitマジックコマンドは非常に便利な機能です。

しかし、時々このコマンドが見つからないというエラーが発生することがあります。

具体的には、次のようなエラーメッセージが表示されます。

UsageError: Line magic function %%timeit not found.

このエラーが発生する主な原因は、IPythonがうまくロードされていないことです。

解決策として、まずIPythonが正しくインストールされているか確認しましょう。

次に、Jupyter Notebookを再起動してみてください。

それでも解決しない場合は、次のコードを実行してみてください。

%load_ext timeit

このコードは、timeit拡張機能を明示的にロードします。

多くの場合、この操作でエラーが解消されます。

もし上記の方法でも解決しない場合は、Anaconda環境を使用している方は、次のコマンドでIPythonを更新してみてください。

conda update ipython

これらの方法を試しても問題が解決しない場合は、Jupyter Notebookの環境に問題がある可能性があります。

その場合は、新しい仮想環境を作成し、必要なパッケージを再インストールすることをおすすめします。

○timeitでメモリエラーが発生した場合の対処法

大規模なデータセットやメモリを大量に使用する関数を計測する際、メモリエラーが発生することがあります。

具体的には、次のようなエラーメッセージが表示されることがあります。

MemoryError: Unable to allocate array with shape (1000000000,) and data type int64

このエラーは、計測対象の関数がシステムの利用可能なメモリを超える量のメモリを要求した場合に発生します。

対処法として、次の方法を試してみてください。

□計測回数の削減

timeit関数のnumberパラメータを小さくすることで、メモリ使用量を減らすことができます。

import timeit

def memory_intensive_function():
    return [i for i in range(100000000)]

# 計測回数を減らす
execution_time = timeit.timeit(memory_intensive_function, number=1)
print(f"実行時間: {execution_time:.6f}秒")

□サンプルデータの縮小

可能であれば、テストに使用するデータセットのサイズを縮小してみてください。

import timeit

def process_data(data):
    return [i**2 for i in data]

# サンプルデータのサイズを小さくする
small_data = list(range(1000000))  # 1000万個から100万個に縮小

execution_time = timeit.timeit(lambda: process_data(small_data), number=10)
print(f"実行時間: {execution_time:.6f}秒")

□ガベージコレクションの強制実行

メモリ使用量が増大する関数を計測する場合、各反復の後にガベージコレクションを強制的に実行することで、メモリの解放を促進できます。

import timeit
import gc

def memory_intensive_function():
    return [i for i in range(10000000)]

def timed_function():
    result = memory_intensive_function()
    gc.collect()  # ガベージコレクションを強制実行
    return result

execution_time = timeit.timeit(timed_function, number=10)
print(f"実行時間: {execution_time:.6f}秒")

○大規模なコードブロックを計測する際のテクニック

大規模なプロジェクトやコンプレックスな関数を扱う際、timeitで全体を一度に計測するのが難しい場合があります。

そのような状況では、コードを小さな部分に分割し、それぞれを個別に計測する方法が効果的です。

□プロファイリングの活用

cProfileモジュールを使用すると、関数単位でパフォーマンスを分析できます。

import cProfile
import pstats
import io

def complex_function():
    result = 0
    for i in range(1000000):
        result += i
    return result

# プロファイリングの実行
pr = cProfile.Profile()
pr.enable()
complex_function()
pr.disable()

# 結果の整形と表示
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
ps.print_stats()
print(s.getvalue())

□デコレータを使用した部分的な計測

大規模な関数内の特定の部分だけを計測したい場合、カスタムデコレータを使用すると便利です。

import time
from functools import wraps

def measure_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__}の実行時間: {end - start:.6f}秒")
        return result
    return wrapper

def complex_function():
    # 計測したい部分だけをデコレータで囲む
    @measure_time
    def critical_section():
        return sum(i**2 for i in range(1000000))

    result = critical_section()
    # その他の処理...
    return result

complex_function()

□コンテキストマネージャを使用した柔軟な計測

with文を使用することで、コード内の任意の部分の実行時間を計測できます。

import time
from contextlib import contextmanager

@contextmanager
def measure_time():
    start = time.perf_counter()
    yield
    end = time.perf_counter()
    print(f"実行時間: {end - start:.6f}秒")

def complex_function():
    # 前処理...

    with measure_time():
        # 計測したい処理
        result = sum(i**2 for i in range(1000000))

    # 後処理...
    return result

complex_function()

●timeit以外の時間計測手法

Pythonにおいて、コードの実行時間を計測する方法はtimeitだけではありません。

ここでは、皆さんが日々のWeb開発やデータ分析業務で直面する様々な状況に対応するため、他の計測手法も習得しておくことが重要です。

ここでは、timeit以外の時間計測手法について、具体的な使用例を交えながら詳しく解説していきます。

○time.perf_counterを使った高精度計測

time.perf_counterは、Pythonの標準ライブラリtimeモジュールに含まれる関数で、高分解能のパフォーマンスカウンターを使用して時間を計測します。

この関数は、特に短い時間間隔を測定する場合に非常に有用です。

time.perf_counterの特徴は、システム時刻の変更やサマータイムの影響を受けないことです。

そのため、長時間にわたる処理の計測にも適しています。

具体的な使用例を見てみましょう。

import time

def complex_calculation():
    return sum(i**2 for i in range(10**6))

start_time = time.perf_counter()
result = complex_calculation()
end_time = time.perf_counter()

execution_time = end_time - start_time
print(f"実行時間: {execution_time:.6f}秒")
print(f"計算結果: {result}")

このコードを実行すると、次のような結果が得られます。

実行時間: 0.123456秒
計算結果: 333332833333500000

time.perf_counterは、開始時刻と終了時刻をそれぞれ記録し、その差分を計算することで処理時間を求めます。

この方法は、単一の処理やループの実行時間を計測する場合に特に有効です。

また、time.perf_counterは浮動小数点数で結果を返すため、ナノ秒単位の精度で時間を計測できます。

大規模プロジェクトでのパフォーマンスチューニングにおいて、この高い精度は非常に重要になってきます。

○cProfileを使ったプロファイリング

cProfileは、Pythonの標準ライブラリに含まれるプロファイリングツールです。

関数単位でコードの実行時間を分析できるため、大規模なプログラムのボトルネックを特定するのに役立ちます。

cProfileの特徴は、プログラム全体の実行時間だけでなく、各関数の呼び出し回数や実行時間も詳細に分析できることです。

これで、最適化すべき箇所を正確に把握することができます。

ここでは、cProfileを使用した具体的な例を紹介します。

import cProfile
import pstats
import io

def function_a(n):
    return sum(i**2 for i in range(n))

def function_b(n):
    return [i**3 for i in range(n)]

def main():
    result_a = function_a(10**5)
    result_b = function_b(10**5)
    return result_a, result_b

# プロファイリングの実行
pr = cProfile.Profile()
pr.enable()
main()
pr.disable()

# 結果の整形と表示
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
ps.print_stats()
print(s.getvalue())

このコードを実行すると、次のような結果が得られます。

         4 function calls in 0.123 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.123    0.123 <string>:1(<module>)
        1    0.000    0.000    0.123    0.123 <ipython-input>:10(main)
        1    0.078    0.078    0.078    0.078 <ipython-input>:4(function_a)
        1    0.045    0.045    0.045    0.045 <ipython-input>:7(function_b)

この結果から、function_aがfunction_bよりも実行時間が長いことがわかります。

また、main関数の実行時間が全体の処理時間とほぼ同じであることも確認できます。

cProfileを使用することで、プログラム全体のパフォーマンスを俯瞰的に把握し、最適化の余地がある関数を特定することができます。

これは、大規模プロジェクトでのパフォーマンスチューニングにおいて非常に有用なスキルとなります。

○line_profilerによる行単位の計測

line_profilerは、Pythonの外部ライブラリで、コードの各行の実行時間を計測することができます。

関数単位ではなく行単位で実行時間を分析できるため、より細かいレベルでのパフォーマンス最適化が可能になります。

line_profilerを使用するには、まず次のコマンドでインストールする必要があります。

pip install line_profiler

インストールが完了したら、次のように使用できます。

from line_profiler import LineProfiler

def time_consuming_function():
    total = 0
    for i in range(1000000):
        total += i**2
    return total

lp = LineProfiler()
lp_wrapper = lp(time_consuming_function)
result = lp_wrapper()
lp.print_stats()

このコードを実行すると、次のような結果が得られます。

Timer unit: 1e-06 s

Total time: 0.123456 s
File: <ipython-input-1-abcdef123456>
Function: time_consuming_function at line 3

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     3                                           def time_consuming_function():
     4         1          1.0      1.0      0.0      total = 0
     5   1000001     100000.0      0.1     81.0      for i in range(1000000):
     6   1000000      23455.0      0.0     19.0          total += i**2
     7         1          0.0      0.0      0.0      return total

この結果から、ループ内の処理(6行目)が全体の実行時間の大部分を占めていることがわかります。

line_profilerを使用することで、コード内のどの行が最も時間を消費しているかを正確に把握し、ピンポイントで最適化を行うことができます。

まとめ

Pythonのtimeitモジュールを使用したコードの処理時間計測について、詳細に解説してきました。

エンジニアの皆さんにとって、この知識は日々のWeb開発やデータ分析業務で大いに役立つ内容であると考えています。

本記事で学んだ内容を実践することで、Pythonコードのパフォーマンス改善やコードの最適化技術を身につけることができます。

新しい技術や手法が常に登場しているため、学習を続け、常に最新の知識を取り入れていくことが大切です。