第22章 パフォーマンス

コードの最適化

Pythonでのコード最適化は、実行速度の向上やメモリ使用量の削減に焦点を当てたプロセスです。具体的な事例を用いて、いくつかの一般的な最適化手法を説明します。

事例1: ループの最適化

不効率なコード例: リスト内の要素を合計する単純な例ですが、この方法はPythonの強みを活かしていません。

numbers = [1, 2, 3, 4, 5]
total = 0
for number in numbers:
    total += number

最適化後のコード: Pythonの組み込み関数sum()を使用することで、同じ処理をより効率的に行えます。

numbers = [1, 2, 3, 4, 5]
total = sum(numbers)


事例2: 条件文の最適化

不効率なコード例: 複数の条件文があり、頻繁に評価される場合、効率が悪くなることがあります。

def is_valid_user(user):
    if user.age > 18 and user.age < 60:
        if user.has_valid_id and user.account_active:
            return True
    return False

最適化後のコード: 条件を一つの式にまとめることで、読みやすく、効率的なコードになります。

def is_valid_user(user):
    return 18 < user.age < 60 and user.has_valid_id and user.account_active


事例3: リスト内包表記の使用

不効率なコード例: リストから別のリストを生成する際に、forループを使っている場合。

original_list = [1, 2, 3, 4, 5]
squared_list = []
for number in original_list:
    squared_list.append(number * number)

最適化後のコード: リスト内包表記を使用することで、コードが簡潔になり、一般的に実行速度も向上します。

original_list = [1, 2, 3, 4, 5]
squared_list = [number * number for number in original_list]


事例4: ジェネレータの利用

不効率なコード例: 全てのデータをリストに格納してから処理する。

def process_data(data):
    results = [perform_computation(d) for d in data]
    return results

large_dataset = range(1000000)
processed_data = process_data(large_dataset)

最適化後のコード: ジェネレータ式を使ってデータを遅延評価し、一度に1つの要素だけを処理します。

def process_data(data):
    for d in data:
        yield perform_computation(d)

large_dataset = range(1000000)
for result in process_data(large_dataset):
    # 必要に応じて1つずつ結果を処理


メモリ管理

Pythonにおけるメモリ管理は、プログラムが効率的にメモリを使用し、リソースの浪費を避けるために重要です。Pythonは自動的にメモリ管理を行いますが、大規模なデータや複雑なアプリケーションではメモリ使用の最適化が必要になることがあります。以下に、Pythonでのメモリ管理の具体的な事例をいくつか示します。

事例1: 大量のデータを扱う際のジェネレータの使用

不効率なコード例: リスト内包表記を使用して、大量のデータをメモリに格納する場合、メモリ使用量が大きくなりすぎる可能性があります。

# 0から9999999までの数値のリストを作成
large_list = [i for i in range(10000000)]

最適化後のコード: ジェネレータ式を使用することで、必要な時にのみデータを生成し、メモリ使用量を削減できます。

# ジェネレータ式を使用
large_generator = (i for i in range(10000000))

# 必要な時にデータを1つずつ処理
for number in large_generator:
    process(number)  # ここで必要な処理を行う


事例2: 不要になったオブジェクトの削除

不効率なコード例: 大きなオブジェクトを使用した後、それが不要になってもそのままにしておくと、メモリが解放されません。

def process_data():
    large_data = load_large_data()  # 大量のデータを読み込み
    result = perform_complex_calculation(large_data)
    return result

result = process_data()
# ここで large_data は不要になるが、メモリはまだ占有されている

最適化後のコード: delステートメントを使用して不要になったオブジェクトを削除し、そのメモリを解放します。

def process_data():
    large_data = load_large_data()
    result = perform_complex_calculation(large_data)
    del large_data  # データを削除してメモリを解放
    return result

result = process_data()


事例3: メモリプロファイリングを使用した最適化

メモリプロファイリング: 特定の関数やコードブロックがどれだけのメモリを使用しているかを特定するために、メモリプロファイラ(例えばmemory_profiler)を使用できます。

from memory_profiler import profile

@profile
def my_function():
    a = [1] * (10 ** 6)
    b = [2] * (2 * 10 ** 7)
    del b
    return a

if __name__ == "__main__":
    my_function()


データベースとAPIの最適化

PythonでのデータベースとAPIの最適化は、アプリケーションのパフォーマンスを大きく左右します。データベースのクエリ効率、APIのレスポンス時間、データ処理の最適化などが主な焦点です。以下に具体的な事例を挙げて説明します。

事例1: データベースクエリの最適化

不効率なクエリの例: 大量のデータを含むデータベースから全てのデータを一度に取得しようとすると、パフォーマンスと応答時間に影響を及ぼす可能性があります。

import sqlite3

# データベースに接続
conn = sqlite3.connect('my_database.db')
cursor = conn.cursor()

# 全てのデータを取得
cursor.execute("SELECT * FROM large_table")
rows = cursor.fetchall()

# データを処理
for row in rows:
    process(row)

conn.close()

最適化後のクエリ: ページネーションや必要なデータのみを取得することで、効率を改善します。

def fetch_data(page_size, page_number):
    offset = page_size * (page_number - 1)
    cursor.execute("SELECT * FROM large_table LIMIT ? OFFSET ?", (page_size, offset))
    return cursor.fetchall()

# ページごとにデータを取得し処理
page_size = 1000
page_number = 1
while True:
    rows = fetch_data(page_size, page_number)
    if not rows:
        break
    for row in rows:
        process(row)
    page_number += 1


事例2: REST APIの最適化

不効率なAPI設計の例: データが多い場合、クライアントが必要としないデータまで送信すると、レスポンス時間が長くなります。

from flask import Flask, jsonify
app = Flask(__name__)

@app.route('/api/data')
def get_data():
    data = retrieve_large_amount_of_data()  # 大量のデータを取得
    return jsonify(data)

最適化後のAPI設計: フィルタリング、ページネーション、データの選択的なロードを実装します。

from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route('/api/data')
def get_data():
    page = request.args.get('page', 1, type=int)
    page_size = request.args.get('page_size', 100, type=int)
    data = retrieve_data_with_pagination(page, page_size)
    return jsonify(data)


事例3: データ処理の最適化

不効率なデータ処理の例: データセット全体をメモリにロードしてから処理すると、大規模なデータセットの場合にメモリの消費が大きくなります。

# 大きなデータファイルの読み込み
with open('large_file.csv', 'r') as file:
    data = file.read()
    process_data(data)  # データを処理

最適化後のデータ処理: データをストリームとして処理し、一度に小さなチャンクを扱います。

# データをチャンクで読み込み、処理
def process_large_file(file_name):
    with open(file_name, 'r') as file:
        for line in file:
            process_line(line)

process_large_file('large_file.csv')


並行処理と並列処理

Pythonでの並行処理と並列処理は、複数のタスクを同時に処理することでプログラムのパフォーマンスを向上させる手法です。これらは似ているように見えますが、実際には異なるアプローチです。

並行処理 (Concurrency)

並行処理は、複数のタスクが交互に実行されるプロセスです。このアプローチは主にI/Oバウンドなタスク(データベース操作、ファイル読み書き、ネットワークリクエストなど)に適しています。

事例: asyncioを使用した非同期プログラミング

import asyncio

async def fetch_data():
    print('データをフェッチ中...')
    await asyncio.sleep(2)  # ネットワーク操作をシミュレート
    print('データフェッチ完了')
    return {'data': 1}

async def print_numbers():
    for i in range(10):
        print(i)
        await asyncio.sleep(1)

async def main():
    task1 = asyncio.create_task(fetch_data())
    task2 = asyncio.create_task(print_numbers())

    value = await task1
    print(value)
    await task2

asyncio.run(main())


並列処理 (Parallelism)

並列処理は、タスクを複数の処理ユニットに分割して同時に実行するプロセスです。CPUバウンドなタスク(重い計算処理など)に適しています。

事例: multiprocessingを使用した並列処理

from multiprocessing import Pool

def square_number(n):
    return n * n

def main():
    numbers = [1, 2, 3, 4, 5]
    with Pool() as pool:
        results = pool.map(square_number, numbers)
        print(results)

if __name__ == "__main__":
    main()


並行処理と並列処理の選択
  • I/Oバウンドなタスク: ネットワークI/OやディスクI/Oなどの待機時間が多いタスクの場合、並行処理(非同期I/O)が適しています。
  • CPUバウンドなタスク: 高負荷の計算処理など、CPUリソースを多く消費するタスクの場合、並列処理が適しています。

Pythonでこれらの手法を適切に使い分けることにより、アプリケーションのパフォーマンスを最大限に引き出すことができます。


練習問題1.

次のコードをより効率的に書き換えてください。

def double_numbers(numbers):
    doubled = []
    for number in numbers:
        doubled.append(number * 2)
    return doubled

# この関数をテストするための大きなリストを作成
large_list = list(range(1000000))
result = double_numbers(large_list)


練習問題2.

次の関数をマルチスレッディングを使って同時に実行し、実行時間を測定して比較してください

import time
import threading

def io_bound_task():
    print("IOタスク開始")
    time.sleep(5)  # 5秒間のI/O待ちをシミュレート
    print("IOタスク終了")

start_time = time.time()
io_bound_task()
io_bound_task()
end_time = time.time()
print(f"シングルスレッドでの実行時間: {end_time - start_time} 秒")

# ここで、マルチスレッディングを使って同じタスクを並行して実行するコードを書いてください。


練習問題3.

1から1000000までの数を合計するコードをメモリ効率良く書き換えてください。

def sum_large_data():
    large_data = list(range(1, 1000001))  # 大きなデータセットを生成
    return sum(large_data)

result = sum_large_data()
print(result)