mcpField
P
python-profiling

python-profiling

github.com/claude-dev-suite開発4636 回インストール2 日前に更新
リポジトリ

python-profiling について

Python 開発者向けのパフォーマンス分析スキルです。CPU とメモリのボトルネックを調べる手法、ベンチマーク、最適化の指針を扱います。

主な機能

  1. 01cProfile による CPU 分析
  2. 02tracemalloc によるメモリ分析
  3. 03行単位のプロファイリング
  4. 04timeit と perf_counter による計測
  5. 05ボトルネックとアンチパターンの指針

活用シーン

  1. 01Python スクリプトの CPU ホットスポット特定
  2. 02メモリ増加やリークの調査
  3. 03短いコード実装の比較
  4. 04性能問題に応じた分析ツールの選択

MCP 対応

Python のプロファイリング資料を得る手段として `mcp__documentation__fetch_docs` に言及しています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill python-profiling

インストール後、Claude Code で/python-profilingを入力して読み込まれたか確認します。

詳細

Python のパフォーマンスプロファイリング

このスキルを使用しない場合

  • Java/JVM のプロファイリング - JFR と GC のチューニングには java-profiling スキルを使用してください
  • Node.js のプロファイリング - V8 プロファイラーには nodejs-profiling スキルを使用してください
  • NumPy/Pandas の最適化 - ライブラリ固有のプロファイリングツールとベクトル化ガイドを使用してください
  • データベースクエリの最適化 - データベース固有のプロファイリングツールを使用してください
  • Web サーバーのパフォーマンス - アプリケーションレベルのプロファイリングを使用してください(Django Debug Toolbar、Flask-DebugToolbar)

詳しい知識:包括的なプロファイリングガイド、最適化手法、ベストプラクティスについては、テクノロジーに python を指定して mcp__documentation__fetch_docs を使用してください。

cProfile(CPU プロファイリング)

コマンドラインの使用方法
# Profile entire script
python -m cProfile -o output.prof script.py

# Sort by cumulative time
python -m cProfile -s cumtime script.py

# Sort by total time in function
python -m cProfile -s tottime script.py

# Analyze saved profile
python -m pstats output.prof
pstats による分析
import pstats

# Load and analyze profile
stats = pstats.Stats('output.prof')
stats.strip_dirs()
stats.sort_stats('cumulative')
stats.print_stats(20)  # Top 20 functions

# Filter by module
stats.print_stats('mymodule')

# Show callers
stats.print_callers('slow_function')

# Show callees
stats.print_callees('main')
プログラムによるプロファイリング
import cProfile
import pstats
from io import StringIO

def profile_function(func, *args, **kwargs):
    profiler = cProfile.Profile()
    profiler.enable()

    result = func(*args, **kwargs)

    profiler.disable()

    # Analyze
    stream = StringIO()
    stats = pstats.Stats(profiler, stream=stream)
    stats.sort_stats('cumulative')
    stats.print_stats(10)
    print(stream.getvalue())

    return result

# Context manager
from contextlib import contextmanager

@contextmanager
def profile_block(name='profile'):
    profiler = cProfile.Profile()
    profiler.enable()
    try:
        yield
    finally:
        profiler.disable()
        profiler.dump_stats(f'{name}.prof')

メモリプロファイリング

tracemalloc(組み込み)
import tracemalloc

# Start tracking
tracemalloc.start()

# Your code here
result = process_data()

# Get snapshot
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("Top 10 memory allocations:")
for stat in top_stats[:10]:
    print(stat)

# Compare snapshots
snapshot1 = tracemalloc.take_snapshot()
# ... code ...
snapshot2 = tracemalloc.take_snapshot()

diff = snapshot2.compare_to(snapshot1, 'lineno')
for stat in diff[:10]:
    print(stat)

# Stop tracking
tracemalloc.stop()
memory_profiler(行単位)
# Install: pip install memory_profiler

from memory_profiler import profile

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

# Command line usage
# python -m memory_profiler script.py

# Profile specific function
# mprof run script.py
# mprof plot
objgraph(オブジェクト参照)
# Install: pip install objgraph

import objgraph

# Most common types
objgraph.show_most_common_types(limit=20)

# Growth since last call
objgraph.show_growth()

# Find reference chain (memory leak detection)
objgraph.show_backrefs([leaked_object], filename='refs.png')

ラインプロファイラー

# Install: pip install line_profiler

# Decorate functions to profile
@profile
def slow_function():
    total = 0
    for i in range(1000000):
        total += i
    return total

# Run with: kernprof -l -v script.py

高精度な計測

time モジュール
import time

# Monotonic clock (best for measuring durations)
start = time.perf_counter()
result = do_work()
duration = time.perf_counter() - start
print(f"Duration: {duration:.4f}s")

# Nanosecond precision (Python 3.7+)
start = time.perf_counter_ns()
result = do_work()
duration_ns = time.perf_counter_ns() - start
print(f"Duration: {duration_ns}ns")
timeit モジュール
import timeit

# Time small code snippets
duration = timeit.timeit('sum(range(1000))', number=10000)
print(f"Average: {duration / 10000:.6f}s")

# Compare implementations
setup = "data = list(range(10000))"
time1 = timeit.timeit('sum(data)', setup, number=1000)
time2 = timeit.timeit('sum(x for x in data)', setup, number=1000)
print(f"sum(): {time1:.4f}s, generator: {time2:.4f}s")

よくあるボトルネックのパターン

リスト操作
# ❌ Bad: Concatenating lists in loop
result = []
for item in items:
    result = result + [process(item)]  # O(n²)

# ✅ Good: Use append
result = []
for item in items:
    result.append(process(item))  # O(n)

# ✅ Better: List comprehension
result = [process(item) for item in items]

# ❌ Bad: Checking membership in list
if item in large_list:  # O(n)
    pass

# ✅ Good: Use set for membership
large_set = set(large_list)
if item in large_set:  # O(1)
    pass
文字列操作
# ❌ Bad: String concatenation in loop
result = ""
for s in strings:
    result += s  # Creates new string each time

# ✅ Good: Use join
result = "".join(strings)

# ❌ Bad: Format in loop
for item in items:
    log(f"Processing {item}")

# ✅ Good: Lazy formatting
import logging
for item in items:
    logging.debug("Processing %s", item)  # Only formats if needed
辞書操作
# ❌ Bad: Repeated key lookup
if key in d:
    value = d[key]
    process(value)

# ✅ Good: Use get or setdefault
value = d.get(key)
if value is not None:
    process(value)

# ❌ Bad: Checking then setting
if key not in d:
    d[key] = []
d[key].append(value)

# ✅ Good: Use defaultdict
from collections import defaultdict
d = defaultdict(list)
d[key].append(value)
ジェネレーターとリスト
# ❌ Bad: Creating large intermediate lists
result = sum([x * 2 for x in range(10_000_000)])  # Uses memory

# ✅ Good: Use generator
result = sum(x * 2 for x in range(10_000_000))  # Lazy evaluation

# Process large files
# ❌ Bad
data = open('large.csv').readlines()  # All in memory
for line in data:
    process(line)

# ✅ Good
with open('large.csv') as f:  # Stream line by line
    for line in f:
        process(line)

NumPy の最適化

import numpy as np

# ❌ Bad: Python loops over arrays
result = []
for i in range(len(arr)):
    result.append(arr[i] * 2)

# ✅ Good: Vectorized operations
result = arr * 2  # SIMD operations

# ❌ Bad: Creating many temporary arrays
result = (arr1 + arr2) * arr3 / arr4  # 3 temporaries

# ✅ Good: In-place operations when possible
result = arr1.copy()
result += arr2
result *= arr3
result /= arr4

# Use appropriate dtypes
arr = np.array(data, dtype=np.float32)  # Half memory of float64

非同期処理の最適化

import asyncio
import aiohttp

# ❌ Bad: Sequential async
async def fetch_all_sequential(urls):
    results = []
    async with aiohttp.ClientSession() as session:
        for url in urls:
            async with session.get(url) as resp:
                results.append(await resp.text())
    return results

# ✅ Good: Concurrent async
async def fetch_all_concurrent(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [await r.text() for r in responses]

# ✅ Better: With concurrency limit
from asyncio import Semaphore

async def fetch_with_limit(urls, limit=10):
    semaphore = Semaphore(limit)

    async def fetch_one(url):
        async with semaphore:
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as resp:
                    return await resp.text()

    return await asyncio.gather(*[fetch_one(url) for url in urls])

マルチプロセッシング

from multiprocessing import Pool, cpu_count
from concurrent.futures import ProcessPoolExecutor

# CPU-bound work
def cpu_intensive(x):
    return sum(i * i for i in range(x))

# Using Pool
with Pool(cpu_count()) as pool:
    results = pool.map(cpu_intensive, range(100))

# Using ProcessPoolExecutor
with ProcessPoolExecutor() as executor:
    results = list(executor.map(cpu_intensive, range(100)))

# Shared memory (Python 3.8+)
from multiprocessing import shared_memory
import numpy as np

# Create shared array
shm = shared_memory.SharedMemory(create=True, size=arr.nbytes)
shared_arr = np.ndarray(arr.shape, dtype=arr.dtype, buffer=shm.buf)
shared_arr[:] = arr[:]

プロファイリングのチェックリスト

確認項目ツールコマンド
CPU のホットスポットcProfilepython -m cProfile script.py
行単位line_profilerkernprof -l -v script.py
メモリ使用量tracemalloctracemalloc.start()
行ごとのメモリ使用量memory_profiler@profile デコレーター
オブジェクト参照objgraphobjgraph.show_growth()
簡易ベンチマークtimeittimeit.timeit()

py-spy(サンプリングプロファイラー)

# Install: pip install py-spy

# Record profile
py-spy record -o profile.svg -- python script.py

# Top-like view of running process
py-spy top --pid <pid>

# Dump current stack
py-spy dump --pid <pid>

# Profile subprocesses
py-spy record --subprocesses -o profile.svg -- python script.py

本番環境での最適化

# Use __slots__ for memory efficiency
class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Use lru_cache for memoization
from functools import lru_cache

@lru_cache(maxsize=1000)
def expensive_computation(x):
    return x ** 2

# Use dataclasses with slots (Python 3.10+)
from dataclasses import dataclass

@dataclass(slots=True)
class Point:
    x: float
    y: float

アンチパターン

アンチパターン問題点正しい方法
ループ内で + を使って文字列を連結する時間計算量が O(n²)''.join() またはリスト内包表記を使用する
ジェネレーターで十分な場面でリスト内包表記を使う不要なメモリ割り当てが発生する1 回限りの反復にはジェネレーター式を使用する
enumerate() が必要な場面で range() を使うインデックスを手動で追跡するため、エラーが起きやすいインデックスと値の取得には enumerate() を使用する
リスト内の要素を検索する検索に O(n) かかるO(1) でメンバーシップを判定できる set を使用する
global 変数を多用するプロファイリングが難しくなり、副作用も発生するパラメーターを渡し、戻り値を返す
数値計算に NumPy を使用しない桁違いに遅くなる配列操作には NumPy でベクトル化する
早すぎる最適化労力が無駄になり、保守も難しくなるまずプロファイリングを行い、ボトルネックを最適化する
import * を使用する名前空間が汚染され、インポートも遅くなる必要な名前を個別にインポートする
サイズが分かっている場合にループ内で .append() を使う再割り当てが複数回発生するリスト内包表記または [None] * size で事前に割り当てる
インスタンスが多いクラスで __slots__ を使用しないメモリ使用量が増えるインスタンスが多いクラスでは __slots__ を使用する

すぐにできるトラブルシューティング

問題診断解決策
大きなデータに対するループが遅いPython のループは低速ですNumPy でベクトル化するか、リスト内包表記を使用します
メモリ使用量が多い大きな中間オブジェクトを作成していますジェネレーターを使用するか、チャンク単位で処理します
GIL の競合マルチスレッド化しても CPU 処理は高速化しませんCPU バウンドのタスクには multiprocessing を使用します
インポートが遅い副作用のある大きなモジュールです遅延インポートを使い、モジュールレベルのコードを減らします
メモリリークオブジェクトがガベージコレクションされていません循環参照を確認し、weakref を使用します
RecursionError再帰が深すぎますsys.setrecursionlimit() で上限を引き上げるか、反復処理に書き換えます
辞書操作が遅いハッシュ衝突が発生していますキーがハッシュ可能で、ハッシュ値が適切に分散されることを確認します
プロファイラーで CPU 使用率が高いC 拡張が表示されませんpy-spy などのサンプリングプロファイラーを使用します
大きなファイルでメモリ不足になるファイル全体を読み込んでいますwith open() を使用し、1 行ずつ処理します
JSON の解析が遅いJSON ファイルが大きすぎますストリーミングパーサー(ijson)または pandas を使用します

関連するスキル

コメント