Learning

Decorators Deep Dive

Decorators Deep Dive

Basic Decorators

python
def uppercase(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() return wrapper @uppercase def greet(name): return f'hello {name}' print(greet('alice')) # 'HELLO ALICE'

Decorators with Arguments

python
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): results = [] for _ in range(n): results.append(func(*args, **kwargs)) return results return wrapper return decorator @repeat(3) def say_hi(): return 'hi' print(say_hi()) # ['hi', 'hi', 'hi']

functools.wraps

python
import functools def timer(func): @functools.wraps(func) # Preserves metadata! def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) print(f'{func.__name__} took {time.time()-start:.4f}s') return result return wrapper @timer def slow_function(): """A slow function""" import time time.sleep(0.1) print(slow_function.__name__) # 'slow_function' (not 'wrapper') print(slow_function.__doc__) # 'A slow function'

Class-Based Decorators

python
class CountCalls: def __init__(self, func): self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f'Call #{self.count}') return self.func(*args, **kwargs) @CountCalls def add(a, b): return a + b print(add(1, 2)) # Call #1, then 3 print(add(3, 4)) # Call #2, then 7 print(add.count) # 2

Stacking Decorators

python
def bold(func): def wrapper(*args, **kwargs): return f'**{func(*args, **kwargs)}**' return wrapper def italic(func): def wrapper(*args, **kwargs): return f'_{func(*args, **kwargs)}_' return wrapper @bold @italic # Applied first (bottom-up!) def text(): return 'hello' print(text()) # '**_hello_**'

Common: Retry Decorator

python
def retry(max_attempts=3): def decorator(func): import functools @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts: raise print(f'Attempt {attempt} failed: {e}') return wrapper return decorator
Key Rules
  • •A decorator is a function that takes a function and returns a NEW function — @syntax is just sugar
  • •Always use *args, **kwargs in wrapper to handle any function signature
  • •ALWAYS use @functools.wraps(func) to preserve __name__, __doc__, and other metadata
  • •Decorators with arguments need a 3-level function: outer(config) → decorator(func) → wrapper(*args)
  • •Class-based decorators use __init__(func) to store the function and __call__(*args) to wrap it
  • •Stacked decorators execute bottom-up: @bold @italic means italic is applied first, then bold
Your Task

Create: 1) `@timer` decorator that prints execution time using `time.perf_counter()`. 2) `@validate_types(*expected_types)` decorator factory that checks argument types match expected. 3) `@CountCalls` class-based decorator that counts calls and prints count. Apply `@CountCalls` then `@timer` to a `factorial(n)` function. Demonstrate with valid and invalid type calls.

EditorPython · JSX
PreviewUpdates on Run Tests
Loading preview…
Tests
Should define timer decorator with functools.wraps
Should define validate_types as decorator factory
validate_types should check isinstance
Should define CountCalls class with __call__
Should stack @CountCalls and @timer
Should demonstrate TypeError from validate_types