python-profiling について
Python 開発者向けのパフォーマンス分析スキルです。CPU とメモリのボトルネックを調べる手法、ベンチマーク、最適化の指針を扱います。
主な機能
- 01cProfile による CPU 分析
- 02tracemalloc によるメモリ分析
- 03行単位のプロファイリング
- 04timeit と perf_counter による計測
- 05ボトルネックとアンチパターンの指針
活用シーン
- 01Python スクリプトの CPU ホットスポット特定
- 02メモリ増加やリークの調査
- 03短いコード実装の比較
- 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 のホットスポット | cProfile | python -m cProfile script.py |
| 行単位 | line_profiler | kernprof -l -v script.py |
| メモリ使用量 | tracemalloc | tracemalloc.start() |
| 行ごとのメモリ使用量 | memory_profiler | @profile デコレーター |
| オブジェクト参照 | objgraph | objgraph.show_growth() |
| 簡易ベンチマーク | timeit | timeit.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 を使用します |
関連するスキル
コメント
Markdown 対応