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""")