02_decorators.py

Download
python 382 lines 9.7 KB
  1"""
  2Python Decorators
  3
  4Demonstrates:
  5- Function decorators
  6- Decorators with arguments
  7- Class decorators
  8- functools.wraps
  9- Practical examples (timing, retry, memoize, logging)
 10- Stacking decorators
 11"""
 12
 13import functools
 14import time
 15from typing import Callable, Any, TypeVar
 16
 17
 18def section(title: str) -> None:
 19    """Print a section header."""
 20    print("\n" + "=" * 60)
 21    print(f"  {title}")
 22    print("=" * 60)
 23
 24
 25# =============================================================================
 26# Basic Function Decorator
 27# =============================================================================
 28
 29section("Basic Function Decorator")
 30
 31
 32def simple_decorator(func: Callable) -> Callable:
 33    """A simple decorator that prints before/after function execution."""
 34    def wrapper(*args, **kwargs):
 35        print(f"  [Before calling {func.__name__}]")
 36        result = func(*args, **kwargs)
 37        print(f"  [After calling {func.__name__}]")
 38        return result
 39    return wrapper
 40
 41
 42@simple_decorator
 43def say_hello(name: str) -> str:
 44    print(f"  Hello, {name}!")
 45    return f"Greeted {name}"
 46
 47
 48result = say_hello("Alice")
 49print(f"Result: {result}")
 50
 51
 52# =============================================================================
 53# functools.wraps - Preserving Metadata
 54# =============================================================================
 55
 56section("functools.wraps - Preserving Metadata")
 57
 58
 59def without_wraps(func: Callable) -> Callable:
 60    """Decorator without functools.wraps."""
 61    def wrapper(*args, **kwargs):
 62        return func(*args, **kwargs)
 63    return wrapper
 64
 65
 66def with_wraps(func: Callable) -> Callable:
 67    """Decorator with functools.wraps."""
 68    @functools.wraps(func)
 69    def wrapper(*args, **kwargs):
 70        return func(*args, **kwargs)
 71    return wrapper
 72
 73
 74@without_wraps
 75def func_a():
 76    """Original docstring for func_a."""
 77    pass
 78
 79
 80@with_wraps
 81def func_b():
 82    """Original docstring for func_b."""
 83    pass
 84
 85
 86print(f"without_wraps: __name__={func_a.__name__}, __doc__={func_a.__doc__}")
 87print(f"with_wraps: __name__={func_b.__name__}, __doc__={func_b.__doc__}")
 88
 89
 90# =============================================================================
 91# Timing Decorator
 92# =============================================================================
 93
 94section("Timing Decorator")
 95
 96
 97def timing_decorator(func: Callable) -> Callable:
 98    """Measure function execution time."""
 99    @functools.wraps(func)
100    def wrapper(*args, **kwargs):
101        start = time.perf_counter()
102        result = func(*args, **kwargs)
103        end = time.perf_counter()
104        print(f"  {func.__name__} took {(end - start) * 1000:.4f} ms")
105        return result
106    return wrapper
107
108
109@timing_decorator
110def slow_function(n: int) -> int:
111    """Simulate slow computation."""
112    time.sleep(0.1)
113    return sum(range(n))
114
115
116result = slow_function(1000)
117print(f"Result: {result}")
118
119
120# =============================================================================
121# Decorator with Arguments
122# =============================================================================
123
124section("Decorator with Arguments")
125
126
127def repeat(times: int):
128    """Decorator that repeats function execution."""
129    def decorator(func: Callable) -> Callable:
130        @functools.wraps(func)
131        def wrapper(*args, **kwargs):
132            results = []
133            for i in range(times):
134                print(f"  Iteration {i + 1}:")
135                result = func(*args, **kwargs)
136                results.append(result)
137            return results
138        return wrapper
139    return decorator
140
141
142@repeat(times=3)
143def greet(name: str) -> str:
144    msg = f"    Hello, {name}!"
145    print(msg)
146    return msg
147
148
149results = greet("Bob")
150print(f"All results: {len(results)} greetings")
151
152
153# =============================================================================
154# Retry Decorator
155# =============================================================================
156
157section("Retry Decorator")
158
159
160def retry(max_attempts: int = 3, delay: float = 0.1):
161    """Retry decorator for handling transient failures."""
162    def decorator(func: Callable) -> Callable:
163        @functools.wraps(func)
164        def wrapper(*args, **kwargs):
165            for attempt in range(1, max_attempts + 1):
166                try:
167                    print(f"  Attempt {attempt}/{max_attempts}")
168                    return func(*args, **kwargs)
169                except Exception as e:
170                    print(f"    Failed: {e}")
171                    if attempt == max_attempts:
172                        print(f"    Max attempts reached, giving up")
173                        raise
174                    time.sleep(delay)
175        return wrapper
176    return decorator
177
178
179# Simulated unreliable function
180call_count = 0
181
182
183@retry(max_attempts=4, delay=0.05)
184def unreliable_function():
185    """Fails first 2 times, succeeds on 3rd."""
186    global call_count
187    call_count += 1
188    if call_count < 3:
189        raise ValueError(f"Simulated failure #{call_count}")
190    return "Success!"
191
192
193try:
194    result = unreliable_function()
195    print(f"Final result: {result}")
196except Exception as e:
197    print(f"Final exception: {e}")
198
199
200# =============================================================================
201# Memoization Decorator
202# =============================================================================
203
204section("Memoization Decorator")
205
206
207def memoize(func: Callable) -> Callable:
208    """Cache function results."""
209    cache = {}
210
211    @functools.wraps(func)
212    def wrapper(*args):
213        if args not in cache:
214            print(f"  Computing {func.__name__}{args}...")
215            cache[args] = func(*args)
216        else:
217            print(f"  Returning cached result for {func.__name__}{args}")
218        return cache[args]
219
220    return wrapper
221
222
223@memoize
224def fibonacci(n: int) -> int:
225    """Compute nth Fibonacci number."""
226    if n <= 1:
227        return n
228    return fibonacci(n - 1) + fibonacci(n - 2)
229
230
231print(f"fibonacci(5) = {fibonacci(5)}")
232print(f"fibonacci(5) = {fibonacci(5)}")  # Second call uses cache
233
234
235# Compare with functools.lru_cache
236section("functools.lru_cache")
237
238
239@functools.lru_cache(maxsize=128)
240def fib_cached(n: int) -> int:
241    """Fibonacci with built-in LRU cache."""
242    if n <= 1:
243        return n
244    return fib_cached(n - 1) + fib_cached(n - 2)
245
246
247print(f"fib_cached(10) = {fib_cached(10)}")
248print(f"Cache info: {fib_cached.cache_info()}")
249
250
251# =============================================================================
252# Class Decorator
253# =============================================================================
254
255section("Class Decorator")
256
257
258def singleton(cls):
259    """Singleton decorator - only one instance allowed."""
260    instances = {}
261
262    @functools.wraps(cls)
263    def get_instance(*args, **kwargs):
264        if cls not in instances:
265            print(f"  Creating new instance of {cls.__name__}")
266            instances[cls] = cls(*args, **kwargs)
267        else:
268            print(f"  Returning existing instance of {cls.__name__}")
269        return instances[cls]
270
271    return get_instance
272
273
274@singleton
275class Database:
276    """Singleton database connection."""
277
278    def __init__(self, name: str):
279        self.name = name
280        print(f"  Database '{name}' initialized")
281
282
283db1 = Database("production")
284db2 = Database("production")
285print(f"db1 is db2: {db1 is db2}")
286
287
288# =============================================================================
289# Stacking Decorators
290# =============================================================================
291
292section("Stacking Decorators")
293
294
295def uppercase(func: Callable) -> Callable:
296    """Convert result to uppercase."""
297    @functools.wraps(func)
298    def wrapper(*args, **kwargs):
299        result = func(*args, **kwargs)
300        return result.upper()
301    return wrapper
302
303
304def exclaim(func: Callable) -> Callable:
305    """Add exclamation marks."""
306    @functools.wraps(func)
307    def wrapper(*args, **kwargs):
308        result = func(*args, **kwargs)
309        return f"{result}!!!"
310    return wrapper
311
312
313@exclaim
314@uppercase
315def get_message(name: str) -> str:
316    """Get a greeting message."""
317    return f"hello {name}"
318
319
320# Applied bottom-up: uppercase first, then exclaim
321print(f"get_message('world') = {get_message('world')}")
322
323
324# =============================================================================
325# Logging Decorator
326# =============================================================================
327
328section("Logging Decorator")
329
330
331def log_calls(func: Callable) -> Callable:
332    """Log function calls with arguments and results."""
333    @functools.wraps(func)
334    def wrapper(*args, **kwargs):
335        args_repr = [repr(a) for a in args]
336        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
337        signature = ", ".join(args_repr + kwargs_repr)
338        print(f"  Calling {func.__name__}({signature})")
339        result = func(*args, **kwargs)
340        print(f"  {func.__name__} returned {result!r}")
341        return result
342    return wrapper
343
344
345@log_calls
346def add(x: int, y: int) -> int:
347    """Add two numbers."""
348    return x + y
349
350
351@log_calls
352def greet_person(name: str, greeting: str = "Hello") -> str:
353    """Greet a person."""
354    return f"{greeting}, {name}!"
355
356
357add(3, 5)
358greet_person("Charlie", greeting="Hi")
359
360
361# =============================================================================
362# Summary
363# =============================================================================
364
365section("Summary")
366
367print("""
368Decorator patterns covered:
3691. Basic decorators - wrap function behavior
3702. functools.wraps - preserve function metadata
3713. Decorators with arguments - parametrize behavior
3724. Timing decorator - performance monitoring
3735. Retry decorator - error handling
3746. Memoization - cache results
3757. Class decorators - modify class behavior
3768. Stacking decorators - compose multiple decorators
3779. Logging decorator - debug and audit
378
379Decorators provide clean separation of concerns and reusable
380cross-cutting functionality.
381""")