python

examples

examples.py🐍
"""
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)
Examples - Python Tutorial | DeepML