⚡ 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

TechniqueUse CasePerformance Gain
List ComprehensionsCreating/filtering lists2-3x faster than loops
Set/Dict LookupsChecking membership100x+ faster than lists
Generator ExpressionsMemory-efficient iterationConstant memory usage
Built-in FunctionsCommon operationsOptimized C implementations
String .join()Concatenating strings10x+ faster than +=
Local VariablesFrequent accessFaster 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?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent