⚡ Optimize Performance
Performance optimization makes your code run faster and use less memory. Good optimization starts with measuring performance, identifying bottlenecks, and applying the right techniques to improve speed and efficiency.
import time
# Example of performance optimization
# Slow approach - creating new lists repeatedly
def slow_sum_squares(numbers):
"""Slow way to calculate sum of squares."""
result = []
for num in numbers:
result.append(num * num)
total = 0
for square in result:
total += square
return total
# Fast approach - using generator and built-in functions
def fast_sum_squares(numbers):
"""Fast way to calculate sum of squares."""
return sum(num * num for num in numbers)
# Test performance difference
numbers = list(range(100000))
# Time the slow version
start_time = time.time()
slow_result = slow_sum_squares(numbers)
slow_time = time.time() - start_time
# Time the fast version
start_time = time.time()
fast_result = fast_sum_squares(numbers)
fast_time = time.time() - start_time
print(f"Slow version: {slow_time:.4f} seconds")
print(f"Fast version: {fast_time:.4f} seconds")
print(f"Speedup: {slow_time / fast_time:.1f}x faster")
print(f"Results match: {slow_result == fast_result}")
🎯 Understanding Performance
Performance optimization focuses on making code run faster and use less memory.
Efficient Data Structures
import time
# Compare different data structures for lookups
def test_lookup_performance():
"""Compare list vs set vs dict for lookups."""
# Create test data
items = list(range(10000))
items_set = set(items)
items_dict = {item: True for item in items}
# Test item to search for
search_item = 9999
# Time list lookup (slow - O(n))
start_time = time.time()
for _ in range(1000):
found = search_item in items
list_time = time.time() - start_time
# Time set lookup (fast - O(1))
start_time = time.time()
for _ in range(1000):
found = search_item in items_set
set_time = time.time() - start_time
# Time dict lookup (fast - O(1))
start_time = time.time()
for _ in range(1000):
found = search_item in items_dict
dict_time = time.time() - start_time
print("Lookup Performance:")
print(f"List: {list_time:.4f} seconds")
print(f"Set: {set_time:.4f} seconds ({list_time/set_time:.0f}x faster)")
print(f"Dict: {dict_time:.4f} seconds ({list_time/dict_time:.0f}x faster)")
def efficient_data_processing():
"""Show efficient ways to process data."""
# Sample data
sales_data = [
{'product': 'laptop', 'category': 'electronics', 'price': 999},
{'product': 'mouse', 'category': 'electronics', 'price': 25},
{'product': 'book', 'category': 'education', 'price': 15},
{'product': 'keyboard', 'category': 'electronics', 'price': 75},
{'product': 'course', 'category': 'education', 'price': 200}
]
# Efficient grouping using dict
categories = {}
for item in sales_data:
category = item['category']
if category not in categories:
categories[category] = []
categories[category].append(item)
# Calculate totals efficiently
category_totals = {}
for category, items in categories.items():
total = sum(item['price'] for item in items)
category_totals[category] = total
print("\nCategory Sales:")
for category, total in category_totals.items():
print(f"{category}: ${total}")
return categories
test_lookup_performance()
efficient_data_processing()
List Comprehensions vs Loops
import time
def compare_list_operations():
"""Compare different ways to create and process lists."""
numbers = list(range(50000))
# Method 1: Traditional loop (slower)
start_time = time.time()
squares_loop = []
for num in numbers:
if num % 2 == 0:
squares_loop.append(num * num)
loop_time = time.time() - start_time
# Method 2: List comprehension (faster)
start_time = time.time()
squares_comp = [num * num for num in numbers if num % 2 == 0]
comp_time = time.time() - start_time
# Method 3: Generator expression (memory efficient)
start_time = time.time()
squares_gen = list(num * num for num in numbers if num % 2 == 0)
gen_time = time.time() - start_time
print("List Creation Performance:")
print(f"Loop: {loop_time:.4f} seconds")
print(f"Comprehension: {comp_time:.4f} seconds ({loop_time/comp_time:.1f}x faster)")
print(f"Generator: {gen_time:.4f} seconds")
print(f"Results equal: {squares_loop == squares_comp == squares_gen}")
def efficient_string_operations():
"""Show efficient string operations."""
words = ['hello', 'world', 'python', 'programming', 'optimization']
# Slow way - string concatenation in loop
start_time = time.time()
result_slow = ""
for word in words * 1000: # Repeat for timing
result_slow += word + " "
slow_time = time.time() - start_time
# Fast way - join method
start_time = time.time()
result_fast = " ".join(words * 1000)
fast_time = time.time() - start_time
print(f"\nString Operations:")
print(f"Concatenation: {slow_time:.4f} seconds")
print(f"Join method: {fast_time:.4f} seconds ({slow_time/fast_time:.1f}x faster)")
compare_list_operations()
efficient_string_operations()
📋 Performance Optimization Techniques
Technique | Use Case | Performance Gain |
---|---|---|
List Comprehensions | Creating/filtering lists | 2-3x faster than loops |
Set/Dict Lookups | Checking membership | 100x+ faster than lists |
Generator Expressions | Memory-efficient iteration | Constant memory usage |
Built-in Functions | Common operations | Optimized C implementations |
String .join() | Concatenating strings | 10x+ faster than += |
Local Variables | Frequent access | Faster than global lookup |
🔧 Profiling and Measurement
Simple Timing Functions
import time
from functools import wraps
def timing_decorator(func):
"""Decorator to measure function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
@timing_decorator
def process_data_slow(data):
"""Process data using slow methods."""
result = []
for item in data:
if item % 2 == 0:
result.append(item ** 2)
return result
@timing_decorator
def process_data_fast(data):
"""Process data using fast methods."""
return [item ** 2 for item in data if item % 2 == 0]
def compare_algorithms():
"""Compare algorithm performance."""
# Test with different data sizes
sizes = [1000, 5000, 10000]
for size in sizes:
data = list(range(size))
print(f"\nProcessing {size:,} items:")
slow_result = process_data_slow(data)
fast_result = process_data_fast(data)
print(f"Results match: {slow_result == fast_result}")
def memory_efficient_processing():
"""Show memory-efficient data processing."""
def count_even_numbers_memory_heavy(max_num):
"""Memory-heavy approach - creates full list."""
numbers = list(range(max_num))
even_numbers = [num for num in numbers if num % 2 == 0]
return len(even_numbers)
def count_even_numbers_memory_light(max_num):
"""Memory-light approach - uses generator."""
even_count = sum(1 for num in range(max_num) if num % 2 == 0)
return even_count
# Test with large numbers
test_size = 1000000
print(f"\nCounting even numbers up to {test_size:,}:")
# Time and compare both approaches
start_time = time.time()
result1 = count_even_numbers_memory_heavy(test_size)
heavy_time = time.time() - start_time
start_time = time.time()
result2 = count_even_numbers_memory_light(test_size)
light_time = time.time() - start_time
print(f"Memory-heavy: {heavy_time:.4f} seconds, result: {result1:,}")
print(f"Memory-light: {light_time:.4f} seconds, result: {result2:,}")
print(f"Results match: {result1 == result2}")
compare_algorithms()
memory_efficient_processing()
Caching and Memoization
import time
from functools import lru_cache
# Without caching - slow for repeated calculations
def fibonacci_slow(n):
"""Calculate fibonacci number without caching."""
if n <= 1:
return n
return fibonacci_slow(n-1) + fibonacci_slow(n-2)
# With caching - fast for repeated calculations
@lru_cache(maxsize=None)
def fibonacci_fast(n):
"""Calculate fibonacci number with caching."""
if n <= 1:
return n
return fibonacci_fast(n-1) + fibonacci_fast(n-2)
def test_caching_performance():
"""Compare performance with and without caching."""
test_numbers = [30, 35, 35, 30, 35] # Repeated values
print("Fibonacci Calculation Performance:")
# Test without caching
start_time = time.time()
for num in test_numbers:
result = fibonacci_slow(num)
print(f"fib({num}) = {result}")
slow_time = time.time() - start_time
print(f"Without caching: {slow_time:.4f} seconds")
# Test with caching
start_time = time.time()
for num in test_numbers:
result = fibonacci_fast(num)
print(f"fib({num}) = {result}")
fast_time = time.time() - start_time
print(f"With caching: {fast_time:.4f} seconds")
print(f"Speedup: {slow_time / fast_time:.1f}x faster")
# Simple manual caching example
class SimpleCache:
"""Simple cache implementation for expensive operations."""
def __init__(self):
self._cache = {}
def expensive_calculation(self, x, y):
"""Simulate expensive calculation with caching."""
cache_key = (x, y)
if cache_key in self._cache:
print(f"Cache hit for ({x}, {y})")
return self._cache[cache_key]
print(f"Computing for ({x}, {y})")
# Simulate expensive operation
time.sleep(0.1) # Pretend this takes time
result = x ** 2 + y ** 2
# Store in cache
self._cache[cache_key] = result
return result
def test_simple_cache():
"""Test simple caching implementation."""
cache = SimpleCache()
# Test repeated calculations
test_cases = [(3, 4), (5, 12), (3, 4), (8, 6), (5, 12)]
print("\nSimple Cache Example:")
for x, y in test_cases:
result = cache.expensive_calculation(x, y)
print(f"Result: {result}")
test_caching_performance()
test_simple_cache()
🎯 Key Takeaways
🚀 What's Next?
Learn how to write reliable unit tests to ensure your code works correctly and catch bugs early.
Continue to: Write Unit Tests
Was this helpful?
Track Your Learning Progress
Sign in to bookmark tutorials and keep track of your learning journey.
Your progress is saved automatically as you read.