python
examples
examples.pyšpython
"""
Python Performance & Optimization - Practical Examples
This file demonstrates profiling, timing, and optimization techniques
"""
import time
import timeit
import functools
from collections import deque
from typing import List, Any
import sys
# ============================================================
# 1. TIMING TECHNIQUES
# ============================================================
print("=" * 60)
print("1. TIMING TECHNIQUES")
print("=" * 60)
# Using time module
def simple_timing():
"""Demonstrate basic timing with time module."""
# time.time() - wall clock time
start = time.time()
result = sum(range(100000))
end = time.time()
print(f"time.time(): {end - start:.6f} seconds")
# time.perf_counter() - high precision timer
start = time.perf_counter()
result = sum(range(100000))
end = time.perf_counter()
print(f"time.perf_counter(): {end - start:.6f} seconds")
# time.process_time() - CPU time only
start = time.process_time()
result = sum(range(100000))
end = time.process_time()
print(f"time.process_time(): {end - start:.6f} seconds")
simple_timing()
# Timer context manager
from contextlib import contextmanager
@contextmanager
def timer(name="Operation"):
"""Context manager for timing code blocks."""
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f"{name}: {elapsed:.6f} seconds")
# Using the timer
with timer("Sum of range"):
result = sum(range(100000))
with timer("List comprehension"):
result = [x ** 2 for x in range(10000)]
# ============================================================
# 2. TIMEIT FOR BENCHMARKING
# ============================================================
print("\n" + "=" * 60)
print("2. TIMEIT BENCHMARKING")
print("=" * 60)
# Compare different approaches
print("Comparing string concatenation methods:")
# Method 1: String concatenation with +
time1 = timeit.timeit(
'result = ""; [result := result + str(i) for i in range(100)]',
number=1000
)
print(f" + concatenation: {time1:.4f}s")
# Method 2: Join method
time2 = timeit.timeit(
'"".join(str(i) for i in range(100))',
number=1000
)
print(f" join() method: {time2:.4f}s")
# Method 3: List then join
time3 = timeit.timeit(
'"".join([str(i) for i in range(100)])',
number=1000
)
print(f" list + join(): {time3:.4f}s")
print(f"\njoin() is {time1/time2:.1f}x faster than + concatenation")
# Compare list operations
print("\nComparing list operations:")
setup = "data = list(range(10000))"
tests = [
("x in list", "5000 in data"),
("list.count()", "data.count(5000)"),
("list.index()", "data.index(5000)"),
]
for name, stmt in tests:
t = timeit.timeit(stmt, setup, number=1000)
print(f" {name}: {t:.4f}s")
# ============================================================
# 3. LIST VS SET VS DICT PERFORMANCE
# ============================================================
print("\n" + "=" * 60)
print("3. DATA STRUCTURE PERFORMANCE")
print("=" * 60)
# Membership testing comparison
n = 10000
test_list = list(range(n))
test_set = set(range(n))
test_dict = {i: None for i in range(n)}
print("Membership testing (item in collection):")
# List - O(n)
time_list = timeit.timeit(
f'{n//2} in test_list',
globals={'test_list': test_list},
number=1000
)
print(f" List: {time_list:.6f}s")
# Set - O(1)
time_set = timeit.timeit(
f'{n//2} in test_set',
globals={'test_set': test_set},
number=1000
)
print(f" Set: {time_set:.6f}s")
# Dict - O(1)
time_dict = timeit.timeit(
f'{n//2} in test_dict',
globals={'test_dict': test_dict},
number=1000
)
print(f" Dict: {time_dict:.6f}s")
print(f"\nSet is {time_list/time_set:.0f}x faster than list for membership!")
# ============================================================
# 4. CACHING WITH LRU_CACHE
# ============================================================
print("\n" + "=" * 60)
print("4. CACHING WITH LRU_CACHE")
print("=" * 60)
# Fibonacci without cache (exponential time)
def fib_slow(n):
if n < 2:
return n
return fib_slow(n - 1) + fib_slow(n - 2)
# Fibonacci with cache (linear time)
@functools.lru_cache(maxsize=None)
def fib_fast(n):
if n < 2:
return n
return fib_fast(n - 1) + fib_fast(n - 2)
print("Fibonacci comparison:")
# Time without cache (limit n to keep it reasonable)
start = time.perf_counter()
result1 = fib_slow(30)
time1 = time.perf_counter() - start
print(f" Without cache (n=30): {time1:.4f}s")
# Time with cache
start = time.perf_counter()
result2 = fib_fast(30)
time2 = time.perf_counter() - start
print(f" With cache (n=30): {time2:.6f}s")
# Can now compute much larger values
start = time.perf_counter()
result3 = fib_fast(500)
time3 = time.perf_counter() - start
print(f" With cache (n=500): {time3:.6f}s")
print(f"\nCache info: {fib_fast.cache_info()}")
# Custom memoization decorator
def memoize(func):
"""Simple memoization decorator."""
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
wrapper.cache = cache
wrapper.clear_cache = cache.clear
return wrapper
@memoize
def expensive_calculation(x, y):
"""Simulated expensive calculation."""
time.sleep(0.01) # Simulate work
return x * y + x ** 2
print("\nCustom memoize demo:")
with timer("First call"):
result = expensive_calculation(10, 20)
with timer("Cached call"):
result = expensive_calculation(10, 20)
# ============================================================
# 5. GENERATOR VS LIST PERFORMANCE
# ============================================================
print("\n" + "=" * 60)
print("5. GENERATORS VS LISTS")
print("=" * 60)
import sys
# Memory comparison
n = 1000000
# List comprehension - stores all in memory
list_comp = [x ** 2 for x in range(n)]
list_size = sys.getsizeof(list_comp)
# Generator expression - lazy evaluation
gen_exp = (x ** 2 for x in range(n))
gen_size = sys.getsizeof(gen_exp)
print("Memory usage:")
print(f" List (1M items): {list_size:,} bytes ({list_size / 1024 / 1024:.2f} MB)")
print(f" Generator: {gen_size:,} bytes")
print(f" Memory saved: {(list_size - gen_size) / 1024 / 1024:.2f} MB")
del list_comp # Free memory
# Performance for iteration
print("\nSum of squares (1M items):")
time_list = timeit.timeit(
'sum([x ** 2 for x in range(100000)])',
number=10
)
print(f" List comprehension: {time_list:.4f}s")
time_gen = timeit.timeit(
'sum(x ** 2 for x in range(100000))',
number=10
)
print(f" Generator expression: {time_gen:.4f}s")
# ============================================================
# 6. LOOP OPTIMIZATION
# ============================================================
print("\n" + "=" * 60)
print("6. LOOP OPTIMIZATION")
print("=" * 60)
# Test data
data = list(range(100000))
# Method 1: For loop with append
def loop_append():
result = []
for x in data:
result.append(x ** 2)
return result
# Method 2: List comprehension
def list_comp():
return [x ** 2 for x in data]
# Method 3: Map function
def map_func():
return list(map(lambda x: x ** 2, data))
# Method 4: Local variable optimization
def local_append():
result = []
append = result.append # Local reference
for x in data:
append(x ** 2)
return result
print("Loop optimization comparison:")
for name, func in [
("For loop + append", loop_append),
("List comprehension", list_comp),
("Map function", map_func),
("Local append ref", local_append),
]:
t = timeit.timeit(func, number=100)
print(f" {name}: {t:.4f}s")
# ============================================================
# 7. DEQUE VS LIST FOR QUEUES
# ============================================================
print("\n" + "=" * 60)
print("7. DEQUE VS LIST FOR QUEUES")
print("=" * 60)
# Using list as queue (slow for popleft)
def queue_with_list(n):
queue = []
for i in range(n):
queue.append(i)
while queue:
queue.pop(0) # O(n) operation!
# Using deque (fast for both ends)
def queue_with_deque(n):
queue = deque()
for i in range(n):
queue.append(i)
while queue:
queue.popleft() # O(1) operation
print("Queue operations (10000 items):")
t_list = timeit.timeit(lambda: queue_with_list(10000), number=10)
print(f" List as queue: {t_list:.4f}s")
t_deque = timeit.timeit(lambda: queue_with_deque(10000), number=10)
print(f" Deque: {t_deque:.4f}s")
print(f"\nDeque is {t_list/t_deque:.1f}x faster!")
# ============================================================
# 8. STRING OPERATIONS
# ============================================================
print("\n" + "=" * 60)
print("8. STRING OPERATIONS")
print("=" * 60)
# String concatenation methods
strings = ["word"] * 1000
# Method 1: + operator (bad for many strings)
def concat_plus():
result = ""
for s in strings:
result += s
return result
# Method 2: join (optimal)
def concat_join():
return "".join(strings)
# Method 3: io.StringIO
import io
def concat_stringio():
buffer = io.StringIO()
for s in strings:
buffer.write(s)
return buffer.getvalue()
print("String concatenation (1000 strings):")
for name, func in [
("+ operator", concat_plus),
("join()", concat_join),
("StringIO", concat_stringio),
]:
t = timeit.timeit(func, number=1000)
print(f" {name}: {t:.4f}s")
# F-strings vs format vs %
print("\nString formatting comparison:")
name = "Alice"
age = 30
t1 = timeit.timeit(
'f"{name} is {age} years old"',
globals={'name': name, 'age': age},
number=100000
)
print(f" f-string: {t1:.4f}s")
t2 = timeit.timeit(
'"{} is {} years old".format(name, age)',
globals={'name': name, 'age': age},
number=100000
)
print(f" .format(): {t2:.4f}s")
t3 = timeit.timeit(
'"%s is %d years old" % (name, age)',
globals={'name': name, 'age': age},
number=100000
)
print(f" % operator: {t3:.4f}s")
# ============================================================
# 9. BUILT-IN FUNCTIONS
# ============================================================
print("\n" + "=" * 60)
print("9. BUILT-IN FUNCTIONS PERFORMANCE")
print("=" * 60)
data = list(range(100000))
# sum() vs manual loop
def manual_sum():
total = 0
for x in data:
total += x
return total
t_manual = timeit.timeit(manual_sum, number=100)
t_builtin = timeit.timeit(lambda: sum(data), number=100)
print("Sum comparison:")
print(f" Manual loop: {t_manual:.4f}s")
print(f" Built-in sum(): {t_builtin:.4f}s")
print(f" Built-in is {t_manual/t_builtin:.1f}x faster")
# any() vs manual check
def manual_any():
for x in data:
if x > 50000:
return True
return False
t_manual = timeit.timeit(manual_any, number=100)
t_builtin = timeit.timeit(lambda: any(x > 50000 for x in data), number=100)
print("\nAny check (first match):")
print(f" Manual loop: {t_manual:.4f}s")
print(f" Built-in any(): {t_builtin:.4f}s")
# ============================================================
# 10. SLOTS FOR MEMORY OPTIMIZATION
# ============================================================
print("\n" + "=" * 60)
print("10. __SLOTS__ FOR MEMORY")
print("=" * 60)
class PointNoSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class PointWithSlots:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
# Memory comparison
p1 = PointNoSlots(1, 2)
p2 = PointWithSlots(1, 2)
# Note: sys.getsizeof doesn't count the __dict__
size1 = sys.getsizeof(p1) + sys.getsizeof(p1.__dict__)
size2 = sys.getsizeof(p2)
print(f"Point without __slots__: {size1} bytes")
print(f"Point with __slots__: {size2} bytes")
print(f"Memory saved: {size1 - size2} bytes per instance")
# Performance for creation
t1 = timeit.timeit(
'PointNoSlots(1, 2)',
globals={'PointNoSlots': PointNoSlots},
number=100000
)
t2 = timeit.timeit(
'PointWithSlots(1, 2)',
globals={'PointWithSlots': PointWithSlots},
number=100000
)
print(f"\nCreation time (100K instances):")
print(f" Without slots: {t1:.4f}s")
print(f" With slots: {t2:.4f}s")
# ============================================================
# 11. PROFILING EXAMPLE
# ============================================================
print("\n" + "=" * 60)
print("11. PROFILING EXAMPLE")
print("=" * 60)
import cProfile
import pstats
import io
def slow_function():
"""Function with intentional inefficiencies."""
result = []
for i in range(1000):
# Slow string concatenation
s = ""
for j in range(100):
s += str(j)
result.append(s)
return result
def fast_function():
"""Optimized version."""
result = []
for i in range(1000):
# Fast join
s = "".join(str(j) for j in range(100))
result.append(s)
return result
# Profile the slow function
print("Profiling slow_function:")
profiler = cProfile.Profile()
profiler.enable()
slow_function()
profiler.disable()
# Print stats
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats('cumulative')
stats.print_stats(5)
print(stream.getvalue())
# Compare timing
print("Timing comparison:")
t_slow = timeit.timeit(slow_function, number=10)
t_fast = timeit.timeit(fast_function, number=10)
print(f" Slow function: {t_slow:.4f}s")
print(f" Fast function: {t_fast:.4f}s")
print(f" Speedup: {t_slow/t_fast:.1f}x")
# ============================================================
# 12. PRACTICAL OPTIMIZATION EXAMPLE
# ============================================================
print("\n" + "=" * 60)
print("12. PRACTICAL OPTIMIZATION")
print("=" * 60)
# Problem: Find all duplicate items in a list
# Naive approach - O(n²)
def find_duplicates_naive(items):
duplicates = []
for i, item in enumerate(items):
if item in items[i+1:] and item not in duplicates:
duplicates.append(item)
return duplicates
# Better approach - O(n)
def find_duplicates_optimized(items):
seen = set()
duplicates = set()
for item in items:
if item in seen:
duplicates.add(item)
seen.add(item)
return list(duplicates)
# Counter approach - O(n)
from collections import Counter
def find_duplicates_counter(items):
counts = Counter(items)
return [item for item, count in counts.items() if count > 1]
# Test data
test_data = list(range(5000)) + list(range(100)) # Some duplicates
print("Finding duplicates in 5100 items:")
for name, func in [
("Naive O(n²)", find_duplicates_naive),
("Optimized O(n)", find_duplicates_optimized),
("Counter O(n)", find_duplicates_counter),
]:
t = timeit.timeit(lambda: func(test_data), number=10)
print(f" {name}: {t:.4f}s")
# ============================================================
# SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("PERFORMANCE OPTIMIZATION SUMMARY")
print("=" * 60)
print("""
Key Optimization Techniques Demonstrated:
ā Time measurement (time, timeit)
ā Data structure selection (list vs set vs dict)
ā Caching with lru_cache
ā Generators for memory efficiency
ā Loop optimization (comprehensions, local refs)
ā Deque for queue operations
ā String concatenation (join vs +)
ā Built-in functions (sum, any, all)
ā __slots__ for memory savings
ā Profiling with cProfile
Remember:
1. Always measure before optimizing
2. Optimize the hot path first
3. Choose appropriate data structures
4. Use built-in functions when possible
5. Consider memory vs. speed trade-offs
""")