python
examples
examples.py🐍python
"""
Debugging and Profiling - Examples
Learn professional debugging techniques and performance profiling.
"""
from typing import Optional, Any
import time
import sys
import traceback
import functools
import cProfile
import pstats
import io
import logging
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime
# =============================================================================
# DEBUGGING TECHNIQUES
# =============================================================================
# 1. Strategic Print Debugging
def debug_print(*args, **kwargs):
"""Enhanced debug print with context."""
import inspect
frame = inspect.currentframe()
if frame and frame.f_back:
caller = frame.f_back
filename = caller.f_code.co_filename.split('/')[-1]
lineno = caller.f_lineno
func_name = caller.f_code.co_name
prefix = f"[DEBUG {filename}:{lineno} in {func_name}]"
print(prefix, *args, **kwargs)
def trace_calls(func):
"""Decorator to trace function calls."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"→ Calling {func.__name__}({signature})")
try:
result = func(*args, **kwargs)
print(f"← {func.__name__} returned {result!r}")
return result
except Exception as e:
print(f"✗ {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
# Example usage
@trace_calls
def calculate_factorial(n: int) -> int:
"""Calculate factorial with tracing."""
if n <= 1:
return 1
return n * calculate_factorial(n - 1)
# 2. Logging for Debugging
class DebugLogger:
"""Configurable debug logger."""
def __init__(self, name: str, level: int = logging.DEBUG):
self.logger = logging.getLogger(name)
self.logger.setLevel(level)
# Console handler with formatting
if not self.logger.handlers:
handler = logging.StreamHandler()
handler.setLevel(level)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - '
'%(filename)s:%(lineno)d - %(message)s'
)
handler.setFormatter(formatter)
self.logger.addHandler(handler)
def debug(self, msg: str, *args, **kwargs):
self.logger.debug(msg, *args, **kwargs)
def info(self, msg: str, *args, **kwargs):
self.logger.info(msg, *args, **kwargs)
def warning(self, msg: str, *args, **kwargs):
self.logger.warning(msg, *args, **kwargs)
def error(self, msg: str, *args, **kwargs):
self.logger.error(msg, *args, **kwargs)
def exception(self, msg: str, *args, **kwargs):
self.logger.exception(msg, *args, **kwargs)
# 3. Context Manager for Debugging
@contextmanager
def debug_context(name: str, show_locals: bool = False):
"""Context manager for debugging code blocks."""
print(f"\n{'='*60}")
print(f"ENTERING: {name}")
print(f"{'='*60}")
start_time = time.perf_counter()
try:
yield
except Exception as e:
print(f"\n{'!'*60}")
print(f"ERROR in {name}: {type(e).__name__}: {e}")
print(f"{'!'*60}")
traceback.print_exc()
raise
finally:
elapsed = time.perf_counter() - start_time
print(f"\n{'-'*60}")
print(f"EXITING: {name} (took {elapsed:.4f}s)")
print(f"{'-'*60}\n")
# 4. Variable Inspector
class Inspector:
"""Debug inspector for examining objects."""
@staticmethod
def inspect(obj: Any, name: str = "object") -> None:
"""Print detailed information about an object."""
print(f"\n{'='*60}")
print(f"Inspecting: {name}")
print(f"{'='*60}")
print(f"Type: {type(obj).__name__}")
print(f"ID: {id(obj)}")
print(f"Size: {sys.getsizeof(obj)} bytes")
print(f"Repr: {repr(obj)[:200]}")
# Show attributes
print(f"\nAttributes:")
for attr in dir(obj):
if not attr.startswith('_'):
try:
value = getattr(obj, attr)
if not callable(value):
print(f" {attr}: {repr(value)[:50]}")
except Exception as e:
print(f" {attr}: <error: {e}>")
print(f"{'='*60}\n")
@staticmethod
def compare(obj1: Any, obj2: Any, name1: str = "obj1", name2: str = "obj2") -> None:
"""Compare two objects."""
print(f"\nComparing {name1} vs {name2}:")
print(f" Same type: {type(obj1) == type(obj2)}")
print(f" Equal (==): {obj1 == obj2}")
print(f" Same object (is): {obj1 is obj2}")
print(f" Type 1: {type(obj1).__name__}")
print(f" Type 2: {type(obj2).__name__}")
# =============================================================================
# PROFILING TECHNIQUES
# =============================================================================
# 1. Simple Timer
class Timer:
"""Simple timer for measuring execution time."""
def __init__(self, name: str = "Timer"):
self.name = name
self.start_time: Optional[float] = None
self.elapsed: float = 0
def start(self):
"""Start the timer."""
self.start_time = time.perf_counter()
def stop(self) -> float:
"""Stop the timer and return elapsed time."""
if self.start_time is None:
raise RuntimeError("Timer not started")
self.elapsed = time.perf_counter() - self.start_time
return self.elapsed
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
print(f"{self.name}: {self.elapsed:.6f} seconds")
# 2. Function Profiler
def profile_function(func):
"""Decorator to profile function execution."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
profiler = cProfile.Profile()
profiler.enable()
try:
result = func(*args, **kwargs)
finally:
profiler.disable()
# Print stats
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats('cumulative')
stats.print_stats(20)
print(stream.getvalue())
return result
return wrapper
# 3. Memory Profiler (simplified)
@dataclass
class MemorySnapshot:
"""Memory usage snapshot."""
timestamp: datetime
rss_bytes: int # Resident Set Size
description: str
class MemoryTracker:
"""Track memory usage over time."""
def __init__(self):
self.snapshots: list[MemorySnapshot] = []
def snapshot(self, description: str = "") -> MemorySnapshot:
"""Take a memory snapshot."""
import resource
# Get memory usage (works on Unix)
try:
usage = resource.getrusage(resource.RUSAGE_SELF)
rss = usage.ru_maxrss * 1024 # Convert to bytes
except Exception:
rss = 0
snap = MemorySnapshot(
timestamp=datetime.now(),
rss_bytes=rss,
description=description
)
self.snapshots.append(snap)
return snap
def report(self) -> None:
"""Print memory usage report."""
print("\n" + "="*60)
print("Memory Usage Report")
print("="*60)
for i, snap in enumerate(self.snapshots):
mb = snap.rss_bytes / (1024 * 1024)
diff = ""
if i > 0:
prev = self.snapshots[i-1].rss_bytes
delta = (snap.rss_bytes - prev) / (1024 * 1024)
diff = f" ({delta:+.2f} MB)"
print(f"{snap.timestamp.strftime('%H:%M:%S')} - "
f"{mb:.2f} MB{diff} - {snap.description}")
# 4. Execution Time Statistics
@dataclass
class TimingStats:
"""Statistics for execution times."""
name: str
calls: int
total_time: float
min_time: float
max_time: float
@property
def avg_time(self) -> float:
return self.total_time / self.calls if self.calls > 0 else 0
class TimingProfiler:
"""Collect timing statistics for functions."""
_instance: Optional['TimingProfiler'] = None
def __init__(self):
self.stats: dict[str, TimingStats] = {}
@classmethod
def get_instance(cls) -> 'TimingProfiler':
"""Get singleton instance."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def record(self, name: str, elapsed: float) -> None:
"""Record a timing measurement."""
if name not in self.stats:
self.stats[name] = TimingStats(
name=name,
calls=0,
total_time=0,
min_time=float('inf'),
max_time=0
)
stats = self.stats[name]
stats.calls += 1
stats.total_time += elapsed
stats.min_time = min(stats.min_time, elapsed)
stats.max_time = max(stats.max_time, elapsed)
def report(self) -> None:
"""Print timing report."""
print("\n" + "="*80)
print("Timing Statistics Report")
print("="*80)
print(f"{'Function':<30} {'Calls':>10} {'Total':>12} "
f"{'Avg':>12} {'Min':>12} {'Max':>12}")
print("-"*80)
for name, stats in sorted(self.stats.items(),
key=lambda x: x[1].total_time,
reverse=True):
print(f"{name:<30} {stats.calls:>10} "
f"{stats.total_time:>12.6f} "
f"{stats.avg_time:>12.6f} "
f"{stats.min_time:>12.6f} "
f"{stats.max_time:>12.6f}")
def timed(func):
"""Decorator to collect timing statistics."""
profiler = TimingProfiler.get_instance()
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
profiler.record(func.__name__, elapsed)
return wrapper
# =============================================================================
# DEBUGGING PATTERNS
# =============================================================================
# 1. Defensive Programming
def safe_divide(a: float, b: float, default: float = 0.0) -> float:
"""Safely divide two numbers."""
if b == 0:
logging.warning(f"Division by zero: {a}/{b}, returning {default}")
return default
return a / b
def safe_get(dictionary: dict, *keys, default: Any = None) -> Any:
"""Safely get nested dictionary values."""
current = dictionary
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return default
return current
# 2. Assertion Helpers
def assert_type(value: Any, expected_type: type, name: str = "value") -> None:
"""Assert that a value has the expected type."""
if not isinstance(value, expected_type):
raise TypeError(
f"{name} must be {expected_type.__name__}, "
f"got {type(value).__name__}: {value!r}"
)
def assert_in_range(value: float, min_val: float, max_val: float,
name: str = "value") -> None:
"""Assert that a value is within a range."""
if not (min_val <= value <= max_val):
raise ValueError(
f"{name} must be between {min_val} and {max_val}, got {value}"
)
# 3. Debug-only Code
DEBUG_MODE = True # Toggle for production
def debug_only(func):
"""Decorator that only runs function in debug mode."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
if DEBUG_MODE:
return func(*args, **kwargs)
return wrapper
@debug_only
def validate_state(state: dict) -> None:
"""Validate application state (debug only)."""
required_keys = ['user', 'session', 'data']
missing = [k for k in required_keys if k not in state]
if missing:
raise ValueError(f"Missing state keys: {missing}")
# =============================================================================
# EXCEPTION DEBUGGING
# =============================================================================
class ExceptionTracker:
"""Track and analyze exceptions."""
def __init__(self):
self.exceptions: list[dict] = []
def capture(self, exc: Exception, context: str = "") -> None:
"""Capture an exception with context."""
self.exceptions.append({
'type': type(exc).__name__,
'message': str(exc),
'traceback': traceback.format_exc(),
'context': context,
'timestamp': datetime.now().isoformat()
})
def report(self) -> None:
"""Print exception report."""
print(f"\n{'='*60}")
print(f"Exception Report ({len(self.exceptions)} total)")
print(f"{'='*60}")
for i, exc in enumerate(self.exceptions, 1):
print(f"\n--- Exception {i} ---")
print(f"Type: {exc['type']}")
print(f"Message: {exc['message']}")
print(f"Context: {exc['context']}")
print(f"Time: {exc['timestamp']}")
def exception_handler(func):
"""Decorator to handle and log exceptions."""
tracker = ExceptionTracker()
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
tracker.capture(e, f"in {func.__name__}")
logging.error(f"Exception in {func.__name__}: {e}")
raise
return wrapper
# =============================================================================
# PDB DEBUGGING HELPERS
# =============================================================================
def breakpoint_here():
"""Convenient breakpoint function."""
# In Python 3.7+, you can just use breakpoint()
import pdb
pdb.set_trace()
def conditional_breakpoint(condition: bool, message: str = "") -> None:
"""Break only when condition is True."""
if condition:
if message:
print(f"Breakpoint triggered: {message}")
breakpoint()
# Example debugging session commands (as documentation)
PDB_CHEATSHEET = """
PDB Cheatsheet:
===============
Navigation:
n(ext) - Execute next line
s(tep) - Step into function
r(eturn) - Continue until return
c(ontinue) - Continue execution
q(uit) - Quit debugger
Inspection:
p expr - Print expression
pp expr - Pretty print
l(ist) - Show source code
ll - Show full function
w(here) - Print stack trace
u(p) - Move up in stack
d(own) - Move down in stack
Breakpoints:
b(reak) [file:]lineno [, condition]
tbreak - Temporary breakpoint
cl(ear) - Clear breakpoints
disable - Disable breakpoint
enable - Enable breakpoint
Variables:
a(rgs) - Print function arguments
locals() - Show local variables
globals() - Show global variables
Commands:
commands - Define commands at breakpoint
!statement - Execute Python statement
"""
# =============================================================================
# EXAMPLE: DEBUGGING A REAL PROBLEM
# =============================================================================
class DataProcessor:
"""Example class with debugging instrumentation."""
def __init__(self, debug: bool = False):
self.debug = debug
self.logger = DebugLogger("DataProcessor")
self.timer = TimingProfiler.get_instance()
self.errors: list[str] = []
def _log(self, msg: str) -> None:
if self.debug:
self.logger.debug(msg)
@timed
def process(self, data: list) -> list:
"""Process data with full debugging."""
self._log(f"Processing {len(data)} items")
results = []
for i, item in enumerate(data):
try:
self._log(f"Processing item {i}: {item}")
result = self._process_item(item)
results.append(result)
except Exception as e:
self.errors.append(f"Item {i}: {e}")
self._log(f"Error processing item {i}: {e}")
self._log(f"Processed {len(results)} items successfully")
return results
@timed
def _process_item(self, item: Any) -> Any:
"""Process a single item."""
if item is None:
raise ValueError("Cannot process None")
if isinstance(item, (int, float)):
return item * 2
elif isinstance(item, str):
return item.upper()
elif isinstance(item, list):
return [self._process_item(x) for x in item]
else:
return item
# =============================================================================
# DEMONSTRATION
# =============================================================================
if __name__ == "__main__":
print("Debugging and Profiling Examples")
print("="*60)
# 1. Trace function calls
print("\n1. Tracing function calls:")
result = calculate_factorial(5)
print(f"Result: {result}")
# 2. Using Timer
print("\n2. Using Timer:")
with Timer("Sleep test"):
time.sleep(0.1)
# 3. Debug context
print("\n3. Debug context manager:")
with debug_context("Example block"):
x = sum(range(1000))
print(f"Sum: {x}")
# 4. Inspector
print("\n4. Object inspection:")
sample_data = {"name": "Test", "values": [1, 2, 3]}
Inspector.inspect(sample_data, "sample_data")
# 5. Data processor with debugging
print("\n5. Data processor with debugging:")
processor = DataProcessor(debug=True)
data = [1, "hello", [2, 3], None, 42]
results = processor.process(data)
print(f"Results: {results}")
print(f"Errors: {processor.errors}")
# 6. Timing report
print("\n6. Timing statistics:")
TimingProfiler.get_instance().report()
# Print PDB cheatsheet
print("\n7. PDB Cheatsheet:")
print(PDB_CHEATSHEET)