# Python Performance Optimization — Advanced Reference Advanced optimization techniques including NumPy vectorization, caching, memory management, parallelization, async I/O, database optimization, and benchmarking tools. ## Advanced Optimization ### Pattern 11: NumPy for Numerical Operations ```python import timeit import numpy as np def python_sum(n): """Sum using pure Python.""" return sum(range(n)) def numpy_sum(n): """Sum using NumPy.""" return np.arange(n).sum() n = 1000000 python_time = timeit.timeit(lambda: python_sum(n), number=100) numpy_time = timeit.timeit(lambda: numpy_sum(n), number=100) print(f"Python: {python_time:.4f}s") print(f"NumPy: {numpy_time:.4f}s") print(f"Speedup: {python_time/numpy_time:.2f}x") # Vectorized operations def python_multiply(): """Element-wise multiplication in Python.""" a = list(range(100000)) b = list(range(100000)) return [x * y for x, y in zip(a, b)] def numpy_multiply(): """Vectorized multiplication in NumPy.""" a = np.arange(100000) b = np.arange(100000) return a * b py_time = timeit.timeit(python_multiply, number=100) np_time = timeit.timeit(numpy_multiply, number=100) print(f"\nPython multiply: {py_time:.4f}s") print(f"NumPy multiply: {np_time:.4f}s") print(f"Speedup: {py_time/np_time:.2f}x") ``` ### Pattern 12: Caching with functools.lru_cache ```python from functools import lru_cache import timeit def fibonacci_slow(n): """Recursive fibonacci without caching.""" if n < 2: return n return fibonacci_slow(n-1) + fibonacci_slow(n-2) @lru_cache(maxsize=None) def fibonacci_fast(n): """Recursive fibonacci with caching.""" if n < 2: return n return fibonacci_fast(n-1) + fibonacci_fast(n-2) # Massive speedup for recursive algorithms n = 30 slow_time = timeit.timeit(lambda: fibonacci_slow(n), number=1) fast_time = timeit.timeit(lambda: fibonacci_fast(n), number=1000) print(f"Without cache (1 run): {slow_time:.4f}s") print(f"With cache (1000 runs): {fast_time:.4f}s") # Cache info print(f"Cache info: {fibonacci_fast.cache_info()}") ``` ### Pattern 13: Using __slots__ for Memory ```python import sys class RegularClass: """Regular class with __dict__.""" def __init__(self, x, y, z): self.x = x self.y = y self.z = z class SlottedClass: """Class with __slots__ for memory efficiency.""" __slots__ = ['x', 'y', 'z'] def __init__(self, x, y, z): self.x = x self.y = y self.z = z # Memory comparison regular = RegularClass(1, 2, 3) slotted = SlottedClass(1, 2, 3) print(f"Regular class size: {sys.getsizeof(regular)} bytes") print(f"Slotted class size: {sys.getsizeof(slotted)} bytes") # Significant savings with many instances regular_objects = [RegularClass(i, i+1, i+2) for i in range(10000)] slotted_objects = [SlottedClass(i, i+1, i+2) for i in range(10000)] print(f"\nMemory for 10000 regular objects: ~{sys.getsizeof(regular) * 10000} bytes") print(f"Memory for 10000 slotted objects: ~{sys.getsizeof(slotted) * 10000} bytes") ``` ### Pattern 14: Multiprocessing for CPU-Bound Tasks ```python import multiprocessing as mp import time def cpu_intensive_task(n): """CPU-intensive calculation.""" return sum(i**2 for i in range(n)) def sequential_processing(): """Process tasks sequentially.""" start = time.time() results = [cpu_intensive_task(1000000) for _ in range(4)] elapsed = time.time() - start return elapsed, results def parallel_processing(): """Process tasks in parallel.""" start = time.time() with mp.Pool(processes=4) as pool: results = pool.map(cpu_intensive_task, [1000000] * 4) elapsed = time.time() - start return elapsed, results if __name__ == "__main__": seq_time, seq_results = sequential_processing() par_time, par_results = parallel_processing() print(f"Sequential: {seq_time:.2f}s") print(f"Parallel: {par_time:.2f}s") print(f"Speedup: {seq_time/par_time:.2f}x") ``` ### Pattern 15: Async I/O for I/O-Bound Tasks ```python import asyncio import aiohttp import time import requests urls = [ "https://httpbin.org/delay/1", "https://httpbin.org/delay/1", "https://httpbin.org/delay/1", "https://httpbin.org/delay/1", ] def synchronous_requests(): """Synchronous HTTP requests.""" start = time.time() results = [] for url in urls: response = requests.get(url) results.append(response.status_code) elapsed = time.time() - start return elapsed, results async def async_fetch(session, url): """Async HTTP request.""" async with session.get(url) as response: return response.status async def asynchronous_requests(): """Asynchronous HTTP requests.""" start = time.time() async with aiohttp.ClientSession() as session: tasks = [async_fetch(session, url) for url in urls] results = await asyncio.gather(*tasks) elapsed = time.time() - start return elapsed, results # Async is much faster for I/O-bound work sync_time, sync_results = synchronous_requests() async_time, async_results = asyncio.run(asynchronous_requests()) print(f"Synchronous: {sync_time:.2f}s") print(f"Asynchronous: {async_time:.2f}s") print(f"Speedup: {sync_time/async_time:.2f}x") ``` ## Database Optimization ### Pattern 16: Batch Database Operations ```python import sqlite3 import time def create_db(): """Create test database.""" conn = sqlite3.connect(":memory:") conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)") return conn def slow_inserts(conn, count): """Insert records one at a time.""" start = time.time() cursor = conn.cursor() for i in range(count): cursor.execute("INSERT INTO users (name) VALUES (?)", (f"User {i}",)) conn.commit() # Commit each insert elapsed = time.time() - start return elapsed def fast_inserts(conn, count): """Batch insert with single commit.""" start = time.time() cursor = conn.cursor() data = [(f"User {i}",) for i in range(count)] cursor.executemany("INSERT INTO users (name) VALUES (?)", data) conn.commit() # Single commit elapsed = time.time() - start return elapsed # Benchmark conn1 = create_db() slow_time = slow_inserts(conn1, 1000) conn2 = create_db() fast_time = fast_inserts(conn2, 1000) print(f"Individual inserts: {slow_time:.4f}s") print(f"Batch insert: {fast_time:.4f}s") print(f"Speedup: {slow_time/fast_time:.2f}x") ``` ### Pattern 17: Query Optimization ```python # Use indexes for frequently queried columns """ -- Slow: No index SELECT * FROM users WHERE email = 'user@example.com'; -- Fast: With index CREATE INDEX idx_users_email ON users(email); SELECT * FROM users WHERE email = 'user@example.com'; """ # Use query planning import sqlite3 conn = sqlite3.connect("example.db") cursor = conn.cursor() # Analyze query performance cursor.execute("EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?", ("test@example.com",)) print(cursor.fetchall()) # Use SELECT only needed columns # Slow: SELECT * # Fast: SELECT id, name ``` ## Memory Optimization ### Pattern 18: Detecting Memory Leaks ```python import tracemalloc import gc def memory_leak_example(): """Example that leaks memory.""" leaked_objects = [] for i in range(100000): # Objects added but never removed leaked_objects.append([i] * 100) # In real code, this would be an unintended reference def track_memory_usage(): """Track memory allocations.""" tracemalloc.start() # Take snapshot before snapshot1 = tracemalloc.take_snapshot() # Run code memory_leak_example() # Take snapshot after snapshot2 = tracemalloc.take_snapshot() # Compare top_stats = snapshot2.compare_to(snapshot1, 'lineno') print("Top 10 memory allocations:") for stat in top_stats[:10]: print(stat) tracemalloc.stop() # Monitor memory track_memory_usage() # Force garbage collection gc.collect() ``` ### Pattern 19: Iterators vs Lists ```python import sys def process_file_list(filename): """Load entire file into memory.""" with open(filename) as f: lines = f.readlines() # Loads all lines return sum(1 for line in lines if line.strip()) def process_file_iterator(filename): """Process file line by line.""" with open(filename) as f: return sum(1 for line in f if line.strip()) # Iterator uses constant memory # List loads entire file into memory ``` ### Pattern 20: Weakref for Caches ```python import weakref class CachedResource: """Resource that can be garbage collected.""" def __init__(self, data): self.data = data # Regular cache prevents garbage collection regular_cache = {} def get_resource_regular(key): """Get resource from regular cache.""" if key not in regular_cache: regular_cache[key] = CachedResource(f"Data for {key}") return regular_cache[key] # Weak reference cache allows garbage collection weak_cache = weakref.WeakValueDictionary() def get_resource_weak(key): """Get resource from weak cache.""" resource = weak_cache.get(key) if resource is None: resource = CachedResource(f"Data for {key}") weak_cache[key] = resource return resource # When no strong references exist, objects can be GC'd ``` ## Benchmarking Tools ### Custom Benchmark Decorator ```python import time from functools import wraps def benchmark(func): """Decorator to benchmark function execution.""" @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.6f} seconds") return result return wrapper @benchmark def slow_function(): """Function to benchmark.""" time.sleep(0.5) return sum(range(1000000)) result = slow_function() ``` ### Performance Testing with pytest-benchmark ```python # Install: pip install pytest-benchmark def test_list_comprehension(benchmark): """Benchmark list comprehension.""" result = benchmark(lambda: [i**2 for i in range(10000)]) assert len(result) == 10000 def test_map_function(benchmark): """Benchmark map function.""" result = benchmark(lambda: list(map(lambda x: x**2, range(10000)))) assert len(result) == 10000 # Run with: pytest test_performance.py --benchmark-compare ```