Python Performance Tuning Best Practices¶
Objective: Master senior-level Python performance tuning patterns for production systems. When you need to optimize Python applications for speed and efficiency, when you want to identify and resolve performance bottlenecks, when you need enterprise-grade performance optimization strategiesโthese best practices become your weapon of choice.
Core Principles¶
- Measure First: Always profile before optimizing
- Identify Bottlenecks: Focus on the biggest performance impacts
- Algorithm Optimization: Choose the right algorithms and data structures
- Memory Efficiency: Optimize memory usage and garbage collection
- Concurrency: Leverage parallel processing where appropriate
Profiling and Benchmarking¶
Performance Profiling Tools¶
# python/01-profiling-benchmarking.py
"""
Comprehensive Python performance profiling and benchmarking
"""
import cProfile
import pstats
import time
import memory_profiler
import psutil
import os
from typing import List, Dict, Any, Callable, Optional
from functools import wraps
import tracemalloc
import line_profiler
class PerformanceProfiler:
"""Comprehensive performance profiling tools"""
def __init__(self):
self.profiler = cProfile.Profile()
self.memory_traces = []
def profile_function(self, func: Callable) -> Callable:
"""Decorator to profile a function"""
@wraps(func)
def wrapper(*args, **kwargs):
# Start profiling
self.profiler.enable()
start_time = time.time()
start_memory = psutil.Process().memory_info().rss
# Execute function
result = func(*args, **kwargs)
# Stop profiling
end_time = time.time()
end_memory = psutil.Process().memory_info().rss
self.profiler.disable()
# Record metrics
execution_time = end_time - start_time
memory_used = end_memory - start_memory
print(f"Function: {func.__name__}")
print(f"Execution time: {execution_time:.4f} seconds")
print(f"Memory used: {memory_used / 1024 / 1024:.2f} MB")
return result
return wrapper
def profile_with_cprofile(self, func: Callable, *args, **kwargs) -> Dict[str, Any]:
"""Profile function with cProfile"""
self.profiler.enable()
result = func(*args, **kwargs)
self.profiler.disable()
# Get profiling stats
stats = pstats.Stats(self.profiler)
stats.sort_stats('cumulative')
# Extract key metrics
total_calls = stats.total_calls
total_time = stats.total_tt
return {
"total_calls": total_calls,
"total_time": total_time,
"stats": stats
}
def profile_memory_usage(self, func: Callable, *args, **kwargs) -> Dict[str, Any]:
"""Profile memory usage of a function"""
# Start memory tracing
tracemalloc.start()
# Execute function
result = func(*args, **kwargs)
# Get memory snapshot
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return {
"current_memory": current,
"peak_memory": peak,
"result": result
}
def line_by_line_profile(self, func: Callable, *args, **kwargs) -> Dict[str, Any]:
"""Line-by-line profiling with line_profiler"""
profiler = line_profiler.LineProfiler()
profiler.add_function(func)
# Execute function
result = profiler.runcall(func, *args, **kwargs)
# Get line-by-line stats
profiler.print_stats()
return {
"result": result,
"profiler": profiler
}
class BenchmarkSuite:
"""Comprehensive benchmarking suite"""
def __init__(self):
self.benchmarks = []
def benchmark_function(self, name: str, func: Callable, *args, **kwargs) -> Dict[str, Any]:
"""Benchmark a single function"""
times = []
memory_usage = []
# Warm up
for _ in range(3):
func(*args, **kwargs)
# Benchmark
for _ in range(10):
start_time = time.time()
start_memory = psutil.Process().memory_info().rss
result = func(*args, **kwargs)
end_time = time.time()
end_memory = psutil.Process().memory_info().rss
times.append(end_time - start_time)
memory_usage.append(end_memory - start_memory)
# Calculate statistics
avg_time = sum(times) / len(times)
min_time = min(times)
max_time = max(times)
avg_memory = sum(memory_usage) / len(memory_usage)
benchmark_result = {
"name": name,
"avg_time": avg_time,
"min_time": min_time,
"max_time": max_time,
"avg_memory": avg_memory,
"times": times,
"memory_usage": memory_usage
}
self.benchmarks.append(benchmark_result)
return benchmark_result
def compare_functions(self, functions: List[tuple]) -> Dict[str, Any]:
"""Compare multiple functions"""
results = {}
for name, func, args, kwargs in functions:
result = self.benchmark_function(name, func, *args, **kwargs)
results[name] = result
# Find fastest
fastest = min(results.items(), key=lambda x: x[1]["avg_time"])
return {
"results": results,
"fastest": fastest[0],
"fastest_time": fastest[1]["avg_time"]
}
def generate_report(self) -> str:
"""Generate benchmarking report"""
if not self.benchmarks:
return "No benchmarks to report"
report = "# Performance Benchmark Report\n\n"
for benchmark in self.benchmarks:
report += f"## {benchmark['name']}\n"
report += f"- Average time: {benchmark['avg_time']:.4f}s\n"
report += f"- Min time: {benchmark['min_time']:.4f}s\n"
report += f"- Max time: {benchmark['max_time']:.4f}s\n"
report += f"- Average memory: {benchmark['avg_memory'] / 1024 / 1024:.2f} MB\n\n"
return report
# Usage example
def example_slow_function(n: int) -> List[int]:
"""Example function to profile"""
result = []
for i in range(n):
result.append(i * i)
return result
def example_fast_function(n: int) -> List[int]:
"""Optimized version of the function"""
return [i * i for i in range(n)]
# Profile functions
profiler = PerformanceProfiler()
@profiler.profile_function
def profiled_function():
return example_slow_function(10000)
# Benchmark comparison
benchmark_suite = BenchmarkSuite()
functions_to_compare = [
("slow_function", example_slow_function, (10000,), {}),
("fast_function", example_fast_function, (10000,), {})
]
comparison_results = benchmark_suite.compare_functions(functions_to_compare)
print(benchmark_suite.generate_report())
Memory Profiling¶
# python/02-memory-profiling.py
"""
Advanced memory profiling and optimization
"""
import tracemalloc
import gc
import sys
import psutil
from typing import List, Dict, Any, Optional
from memory_profiler import profile
import objgraph
class MemoryProfiler:
"""Advanced memory profiling tools"""
def __init__(self):
self.memory_snapshots = []
self.gc_stats = []
def start_memory_tracing(self):
"""Start memory tracing"""
tracemalloc.start()
def stop_memory_tracing(self) -> Dict[str, Any]:
"""Stop memory tracing and get stats"""
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return {
"current_memory": current,
"peak_memory": peak,
"current_mb": current / 1024 / 1024,
"peak_mb": peak / 1024 / 1024
}
def get_memory_snapshot(self) -> Dict[str, Any]:
"""Get current memory snapshot"""
process = psutil.Process()
memory_info = process.memory_info()
snapshot = {
"rss": memory_info.rss, # Resident Set Size
"vms": memory_info.vms, # Virtual Memory Size
"percent": process.memory_percent(),
"available_memory": psutil.virtual_memory().available
}
self.memory_snapshots.append(snapshot)
return snapshot
def analyze_memory_usage(self, func: Callable, *args, **kwargs) -> Dict[str, Any]:
"""Analyze memory usage of a function"""
# Get initial memory
initial_snapshot = self.get_memory_snapshot()
# Start tracing
self.start_memory_tracing()
# Execute function
result = func(*args, **kwargs)
# Get final memory
final_snapshot = self.get_memory_snapshot()
trace_stats = self.stop_memory_tracing()
# Calculate memory usage
memory_used = final_snapshot["rss"] - initial_snapshot["rss"]
return {
"initial_memory": initial_snapshot,
"final_memory": final_snapshot,
"memory_used": memory_used,
"trace_stats": trace_stats,
"result": result
}
def find_memory_leaks(self) -> Dict[str, Any]:
"""Find potential memory leaks"""
# Force garbage collection
gc.collect()
# Get object counts
object_counts = objgraph.most_common_types()
# Get reference counts
reference_counts = {}
for obj_type, count in object_counts:
if count > 100: # Threshold for potential leaks
reference_counts[obj_type] = count
return {
"object_counts": object_counts,
"potential_leaks": reference_counts,
"gc_counts": gc.get_count()
}
def optimize_memory_usage(self, func: Callable, *args, **kwargs) -> Dict[str, Any]:
"""Optimize memory usage of a function"""
# Analyze current memory usage
analysis = self.analyze_memory_usage(func, *args, **kwargs)
# Force garbage collection
gc.collect()
# Get memory after GC
post_gc_snapshot = self.get_memory_snapshot()
return {
"before_optimization": analysis,
"after_gc": post_gc_snapshot,
"gc_effectiveness": analysis["final_memory"]["rss"] - post_gc_snapshot["rss"]
}
# Memory optimization patterns
class MemoryOptimizer:
"""Memory optimization patterns and techniques"""
@staticmethod
def use_generators(data: List[Any]) -> List[Any]:
"""Use generators instead of lists for large datasets"""
return (item for item in data if item is not None)
@staticmethod
def use_slots(cls):
"""Use __slots__ to reduce memory usage"""
class OptimizedClass(cls):
__slots__ = ['attr1', 'attr2', 'attr3']
return OptimizedClass
@staticmethod
def lazy_loading(data_source: Callable) -> Any:
"""Implement lazy loading for expensive operations"""
class LazyLoader:
def __init__(self, loader):
self._loader = loader
self._data = None
def __getattr__(self, name):
if self._data is None:
self._data = self._loader()
return getattr(self._data, name)
return LazyLoader(data_source)
@staticmethod
def memory_efficient_processing(data: List[Any], batch_size: int = 1000) -> List[Any]:
"""Process data in batches to reduce memory usage"""
results = []
for i in range(0, len(data), batch_size):
batch = data[i:i + batch_size]
# Process batch
batch_results = [item * 2 for item in batch]
results.extend(batch_results)
# Force garbage collection after each batch
gc.collect()
return results
# Usage example
@profile
def memory_intensive_function(n: int) -> List[int]:
"""Function that uses a lot of memory"""
data = []
for i in range(n):
data.append(i * i)
return data
def optimized_memory_function(n: int) -> List[int]:
"""Memory-optimized version"""
return [i * i for i in range(n)]
# Profile memory usage
memory_profiler = MemoryProfiler()
memory_analysis = memory_profiler.analyze_memory_usage(memory_intensive_function, 10000)
print(f"Memory used: {memory_analysis['memory_used'] / 1024 / 1024:.2f} MB")
Algorithm Optimization¶
Data Structure Optimization¶
# python/03-algorithm-optimization.py
"""
Algorithm and data structure optimization
"""
import time
from typing import List, Dict, Any, Optional, Set
from collections import defaultdict, deque, Counter
import heapq
import bisect
class AlgorithmOptimizer:
"""Algorithm optimization techniques"""
@staticmethod
def optimize_list_operations(data: List[int]) -> List[int]:
"""Optimize list operations"""
# Use list comprehension instead of loops
return [x * 2 for x in data if x > 0]
@staticmethod
def optimize_dictionary_operations(data: List[str]) -> Dict[str, int]:
"""Optimize dictionary operations"""
# Use Counter for counting
return Counter(data)
@staticmethod
def optimize_set_operations(data1: Set[int], data2: Set[int]) -> Set[int]:
"""Optimize set operations"""
# Use set operations for intersection, union, etc.
return data1.intersection(data2)
@staticmethod
def optimize_search_operations(data: List[int], target: int) -> Optional[int]:
"""Optimize search operations using binary search"""
# Use bisect for binary search
index = bisect.bisect_left(data, target)
if index < len(data) and data[index] == target:
return index
return None
@staticmethod
def optimize_heap_operations(data: List[int], k: int) -> List[int]:
"""Optimize heap operations for top-k elements"""
# Use heapq for efficient heap operations
return heapq.nlargest(k, data)
@staticmethod
def optimize_string_operations(text: str) -> str:
"""Optimize string operations"""
# Use join instead of concatenation
return ''.join(char.upper() for char in text if char.isalpha())
class DataStructureOptimizer:
"""Data structure optimization patterns"""
@staticmethod
def use_deque_for_fifo(data: List[Any]) -> deque:
"""Use deque for FIFO operations"""
return deque(data)
@staticmethod
def use_defaultdict_for_grouping(data: List[tuple]) -> Dict[Any, List[Any]]:
"""Use defaultdict for grouping operations"""
grouped = defaultdict(list)
for key, value in data:
grouped[key].append(value)
return grouped
@staticmethod
def use_counter_for_frequency(data: List[Any]) -> Counter:
"""Use Counter for frequency analysis"""
return Counter(data)
@staticmethod
def use_heap_for_priority_queue(data: List[tuple]) -> List[tuple]:
"""Use heap for priority queue operations"""
heapq.heapify(data)
return data
# Performance comparison examples
def compare_search_algorithms():
"""Compare different search algorithms"""
data = list(range(1000000))
target = 500000
# Linear search
start_time = time.time()
linear_result = target in data
linear_time = time.time() - start_time
# Binary search
start_time = time.time()
binary_result = AlgorithmOptimizer.optimize_search_operations(data, target)
binary_time = time.time() - start_time
print(f"Linear search: {linear_time:.6f}s")
print(f"Binary search: {binary_time:.6f}s")
print(f"Speedup: {linear_time / binary_time:.2f}x")
def compare_data_structures():
"""Compare different data structures"""
data = list(range(10000))
# List operations
start_time = time.time()
list_result = [x * 2 for x in data]
list_time = time.time() - start_time
# Set operations
data_set = set(data)
start_time = time.time()
set_result = {x * 2 for x in data_set}
set_time = time.time() - start_time
print(f"List operations: {list_time:.6f}s")
print(f"Set operations: {set_time:.6f}s")
Concurrency Optimization¶
Parallel Processing¶
# python/04-concurrency-optimization.py
"""
Concurrency and parallel processing optimization
"""
import asyncio
import concurrent.futures
import multiprocessing
from typing import List, Callable, Any
import time
import threading
from functools import partial
class ConcurrencyOptimizer:
"""Concurrency optimization patterns"""
@staticmethod
def parallel_processing(data: List[Any], func: Callable, max_workers: int = None) -> List[Any]:
"""Parallel processing with ThreadPoolExecutor"""
if max_workers is None:
max_workers = multiprocessing.cpu_count()
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(func, data))
return results
@staticmethod
def process_pool_processing(data: List[Any], func: Callable, max_workers: int = None) -> List[Any]:
"""Process pool for CPU-intensive tasks"""
if max_workers is None:
max_workers = multiprocessing.cpu_count()
with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(func, data))
return results
@staticmethod
async def async_processing(data: List[Any], func: Callable) -> List[Any]:
"""Async processing for I/O-intensive tasks"""
tasks = [func(item) for item in data]
results = await asyncio.gather(*tasks)
return results
@staticmethod
def batch_processing(data: List[Any], func: Callable, batch_size: int = 100) -> List[Any]:
"""Process data in batches"""
results = []
for i in range(0, len(data), batch_size):
batch = data[i:i + batch_size]
batch_results = [func(item) for item in batch]
results.extend(batch_results)
return results
class AsyncOptimizer:
"""Async optimization patterns"""
@staticmethod
async def async_generator(data: List[Any], func: Callable):
"""Async generator for memory efficiency"""
for item in data:
result = await func(item)
yield result
@staticmethod
async def async_batch_processing(data: List[Any], func: Callable, batch_size: int = 100):
"""Async batch processing"""
results = []
for i in range(0, len(data), batch_size):
batch = data[i:i + batch_size]
batch_tasks = [func(item) for item in batch]
batch_results = await asyncio.gather(*batch_tasks)
results.extend(batch_results)
return results
@staticmethod
async def async_with_semaphore(data: List[Any], func: Callable, max_concurrent: int = 10):
"""Async processing with semaphore for rate limiting"""
semaphore = asyncio.Semaphore(max_concurrent)
async def limited_func(item):
async with semaphore:
return await func(item)
tasks = [limited_func(item) for item in data]
results = await asyncio.gather(*tasks)
return results
# Performance comparison
def cpu_intensive_task(n: int) -> int:
"""CPU-intensive task"""
return sum(i * i for i in range(n))
def io_intensive_task(delay: float) -> str:
"""I/O-intensive task"""
time.sleep(delay)
return f"Task completed after {delay}s"
async def async_io_task(delay: float) -> str:
"""Async I/O-intensive task"""
await asyncio.sleep(delay)
return f"Async task completed after {delay}s"
def compare_concurrency_methods():
"""Compare different concurrency methods"""
data = list(range(100))
# Sequential processing
start_time = time.time()
sequential_results = [cpu_intensive_task(i) for i in data]
sequential_time = time.time() - start_time
# Thread pool processing
start_time = time.time()
thread_results = ConcurrencyOptimizer.parallel_processing(data, cpu_intensive_task)
thread_time = time.time() - start_time
# Process pool processing
start_time = time.time()
process_results = ConcurrencyOptimizer.process_pool_processing(data, cpu_intensive_task)
process_time = time.time() - start_time
print(f"Sequential: {sequential_time:.4f}s")
print(f"Thread pool: {thread_time:.4f}s")
print(f"Process pool: {process_time:.4f}s")
print(f"Thread speedup: {sequential_time / thread_time:.2f}x")
print(f"Process speedup: {sequential_time / process_time:.2f}x")
TL;DR Runbook¶
Quick Start¶
# 1. Profile your code
from python.profiling import PerformanceProfiler
profiler = PerformanceProfiler()
@profiler.profile_function
def your_function():
# Your code here
pass
# 2. Benchmark different approaches
from python.benchmarking import BenchmarkSuite
benchmark = BenchmarkSuite()
benchmark.benchmark_function("approach1", function1)
benchmark.benchmark_function("approach2", function2)
# 3. Optimize memory usage
from python.memory_optimization import MemoryOptimizer
optimizer = MemoryOptimizer()
optimized_data = optimizer.memory_efficient_processing(data)
# 4. Use concurrency for I/O-bound tasks
from python.concurrency import ConcurrencyOptimizer
results = ConcurrencyOptimizer.parallel_processing(data, your_function)
Essential Patterns¶
# Complete performance optimization
def optimize_python_application():
"""Complete performance optimization workflow"""
# 1. Profile to identify bottlenecks
profiler = PerformanceProfiler()
# 2. Benchmark different approaches
benchmark_suite = BenchmarkSuite()
# 3. Optimize memory usage
memory_optimizer = MemoryOptimizer()
# 4. Use appropriate concurrency
concurrency_optimizer = ConcurrencyOptimizer()
# 5. Apply algorithm optimizations
algorithm_optimizer = AlgorithmOptimizer()
print("Performance optimization complete!")
This guide provides the complete machinery for Python performance tuning. Each pattern includes implementation examples, optimization strategies, and real-world usage patterns for enterprise performance optimization.