Python Decorators - Quick Reference Cheat Sheet¶
Basic Syntax¶
Simple Decorator¶
def my_decorator(func):
def wrapper(*args, **kwargs):
# Do something before
result = func(*args, **kwargs)
# Do something after
return result
return wrapper
@my_decorator
def my_function():
pass
With functools.wraps (ALWAYS USE THIS!)¶
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserves function metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Decorator Patterns¶
1. Basic Decorator Template¶
from functools import wraps
def decorator_name(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Before function execution
result = func(*args, **kwargs)
# After function execution
return result
return wrapper
2. Decorator with Parameters¶
def decorator_with_params(param1, param2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Use param1, param2
return func(*args, **kwargs)
return wrapper
return decorator
@decorator_with_params("value1", "value2")
def my_function():
pass
3. Class-Based Decorator¶
from functools import update_wrapper
class MyDecorator:
def __init__(self, func):
update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
# Your logic here
return self.func(*args, **kwargs)
@MyDecorator
def my_function():
pass
Common Use Cases¶
Timing¶
import time
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end-start:.4f}s")
return result
return wrapper
Logging¶
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
Debug¶
def debug(func):
@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})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
Caching/Memoization¶
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
# Or use built-in:
from functools import lru_cache
@lru_cache(maxsize=128)
def my_function(n):
pass
Exception Handling¶
def handle_errors(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error: {e}")
return None
return wrapper
Retry Logic¶
def retry(max_attempts=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Retry {attempt + 1}/{max_attempts}")
return wrapper
return decorator
Validation¶
def validate_positive(func):
@wraps(func)
def wrapper(n):
if n < 0:
raise ValueError("Must be positive")
return func(n)
return wrapper
def validate_type(expected_type):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not isinstance(args[0], expected_type):
raise TypeError(f"Expected {expected_type}")
return func(*args, **kwargs)
return wrapper
return decorator
Rate Limiting¶
import time
def rate_limit(max_calls, time_period):
def decorator(func):
calls = []
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
calls[:] = [t for t in calls if now - t < time_period]
if len(calls) >= max_calls:
return None
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
Authentication¶
def require_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not is_authenticated():
raise PermissionError("Not authenticated")
return func(*args, **kwargs)
return wrapper
def require_role(role):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if user_role != role:
raise PermissionError(f"Requires {role}")
return func(*args, **kwargs)
return wrapper
return decorator
Stacking Decorators¶
@decorator1
@decorator2
@decorator3
def my_function():
pass
# Equivalent to:
# my_function = decorator1(decorator2(decorator3(my_function)))
Important: Decorators are applied bottom-to-top!
Example¶
def uppercase(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
def exclaim(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) + "!"
return wrapper
@uppercase # Applied SECOND
@exclaim # Applied FIRST
def greet():
return "hello"
greet() # Returns "HELLO!"
Built-in Decorators¶
@property¶
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Negative radius")
self._radius = value
@property
def area(self):
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.radius) # Access like attribute
c.radius = 10 # Use setter
print(c.area) # Computed property
@staticmethod¶
class MyClass:
@staticmethod
def static_method():
# No access to self or cls
return "Static!"
MyClass.static_method() # Can call without instance
@classmethod¶
class MyClass:
count = 0
@classmethod
def increment(cls):
cls.count += 1
MyClass.increment() # Access class variables
@functools.lru_cache¶
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Best Practices¶
✅ DO:¶
- Use
@wrapsto preserve function metadata - Use
*args, **kwargsfor flexibility - Keep decorators simple and focused
- Document what decorators do
- Consider using built-in decorators when available
❌ DON'T:¶
- Forget to return the wrapper function
- Modify the original function
- Make decorators too complex
- Use decorators for core business logic
- Stack too many decorators (hard to debug)
Common Patterns Summary¶
| Pattern | Use Case | Complexity |
|---|---|---|
| Simple wrapper | Add behavior before/after | Easy |
| With parameters | Configurable behavior | Medium |
| Class-based | Need state/attributes | Medium |
| Stacked | Multiple behaviors | Easy |
| Memoization | Cache expensive calls | Easy |
| Timing | Performance monitoring | Easy |
| Validation | Input checking | Easy |
| Retry | Handle transient failures | Medium |
| Rate limiting | Control call frequency | Hard |
Quick Tips¶
- Debugging: Decorated functions can be hard to debug - use simple decorators
- Performance: Each decorator adds overhead - use sparingly
- Order matters: When stacking, decorators apply bottom-to-top
- Testing: Test both the decorator and decorated function
- Documentation: Always document what your decorator does
Common Mistakes¶
# ❌ Forgetting to return wrapper
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Missing: return wrapper
# ❌ Not using *args, **kwargs
def bad_decorator(func):
def wrapper(): # Won't work with arguments!
return func()
return wrapper
# ❌ Not using @wraps
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # Loses func.__name__, __doc__, etc.
# ✅ Correct pattern
from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Real-World Examples¶
# Flask route decorator
@app.route('/users/<id>')
def get_user(id):
pass
# Django login required
@login_required
def profile(request):
pass
# pytest fixture
@pytest.fixture
def database():
pass
# Click CLI command
@click.command()
def hello():
pass
Remember: Decorators are syntactic sugar for function wrapping. Master the basics before creating complex decorators!¶
Runnable Example: singledispatch_example.py¶
"""
TUTORIAL: Single Dispatch - Type-Based Function Polymorphism
This tutorial covers functools.singledispatch, a powerful decorator that enables
FUNCTION OVERLOADING based on the type of the first argument.
The Problem: In Python, we often need to handle the same operation differently
depending on the type of data. You could write a giant if/elif/else chain, but
that's ugly and hard to extend.
The Solution: @singledispatch lets you define a generic function, then register
type-specific implementations. When called, Python automatically dispatches to
the right implementation based on the argument type.
This is elegant, extensible, and Pythonic. The htmlize function demonstrates
this perfectly - one function that handles strings, lists, numbers, fractions,
and more, each with custom formatting.
"""
from functools import singledispatch
from collections import abc
import fractions
import decimal
import html
import numbers
if __name__ == "__main__":
print("=" * 70)
print("TUTORIAL: Single Dispatch - Type-Based Function Polymorphism")
print("=" * 70)
# ============ EXAMPLE 1: The Problem Without Singledispatch
print("\n# ============ EXAMPLE 1: The Problem Without Singledispatch")
print("Why we need singledispatch - handling multiple types with if/elif:\n")
def htmlize_bad(obj):
"""Convert any object to HTML - WITHOUT singledispatch"""
if isinstance(obj, str):
# For strings, escape HTML and preserve newlines
content = html.escape(obj).replace('\n', '<br/>\n')
return f'<p>{content}</p>'
elif isinstance(obj, abc.Sequence):
# For sequences, create a list with each item htmlized
inner = '</li>\n<li>'.join(htmlize_bad(item) for item in obj)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
elif isinstance(obj, numbers.Integral):
# For integers, show decimal and hex
return f'<pre>{obj} (0x{obj:x})</pre>'
else:
# Default: escape the repr
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'
print("The bad approach - giant if/elif chain:")
print("Problems:")
print(" 1. Hard to read - all cases mixed together")
print(" 2. Hard to extend - must modify the function itself")
print(" 3. Couples types together - adding a new type means rewriting the function")
print(" 4. No way for external code to add handlers")
print("\nTest the bad version:")
test_str = 'Hello\nWorld'
print(f"htmlize_bad('Hello\\nWorld') = {htmlize_bad(test_str)}")
test_list = [1, 2, 3]
print(f"htmlize_bad([1, 2, 3]) = {htmlize_bad(test_list)}")
print(f"htmlize_bad(42) = {htmlize_bad(42)}")
# ============ EXAMPLE 2: Introduction to Singledispatch
print("\n" + "=" * 70)
print("# ============ EXAMPLE 2: Introduction to Singledispatch")
print("The elegant solution - using @singledispatch:\n")
print("""
@singledispatch CREATES POLYMORPHIC FUNCTIONS
Steps:
1. Define a generic function with @singledispatch decorator
2. Register type-specific implementations with @func.register
3. When called, Python dispatches to the right implementation
4. Much cleaner than if/elif chains!
BENEFITS:
- Each type's logic is separate and focused
- Easy to add new types - just register them
- Extensible - external code can register new types
- Pythonic and elegant
- Type specialization is clear and explicit
""")
# ============ EXAMPLE 3: Basic Singledispatch Example
print("\n" + "=" * 70)
print("# ============ EXAMPLE 3: Basic Singledispatch Example")
print("Building htmlize step-by-step with singledispatch:\n")
@singledispatch
def htmlize(obj: object) -> str:
"""
Generic function to convert any object to HTML.
This is the BASE implementation - handles unknown types.
"""
# For unknown types, just escape the repr
content = html.escape(repr(obj))
return f'<pre>{content}</pre>'
print("Step 1: Define the generic base function")
print(f"htmlize(42) = {htmlize(42)}")
print(f"htmlize({{1, 2, 3}}) = {htmlize({1, 2, 3})}")
@htmlize.register(str)
def _(text: str) -> str:
"""Specialized handler for strings"""
content = html.escape(text).replace('\n', '<br/>\n')
return f'<p>{content}</p>'
print("\nStep 2: Register a handler for str")
print(f"htmlize('Hello') = {htmlize('Hello')}")
hello_world = 'Hello\nWorld'
print(f"htmlize('Hello\\nWorld') = {htmlize(hello_world)}")
@htmlize.register(abc.Sequence)
def _(seq: abc.Sequence) -> str:
"""Specialized handler for sequences (lists, tuples, etc.)"""
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
print("\nStep 3: Register a handler for Sequence")
print(f"htmlize([1, 2, 3]) = {htmlize([1, 2, 3])}")
print(f"htmlize(('a', 'b')) = {htmlize(('a', 'b'))}")
@htmlize.register(numbers.Integral)
def _(n: numbers.Integral) -> str:
"""Specialized handler for integers"""
return f'<pre>{n} (0x{n:x})</pre>'
print("\nStep 4: Register a handler for Integral")
print(f"htmlize(42) = {htmlize(42)}")
print(f"htmlize(255) = {htmlize(255)}")
# ============ EXAMPLE 4: Handling Boolean Special Case
print("\n" + "=" * 70)
print("# ============ EXAMPLE 4: Handling Boolean Special Case")
print("Important: bool is a subtype of int, handle it separately:\n")
print("""
GOTCHA: In Python, bool is a subclass of int!
isinstance(True, int) -> True
isinstance(True, bool) -> True
If you don't register bool, it will use the int handler:
htmlize(True) -> '<pre>1 (0x1)</pre>' <- WRONG!
Solution: Register bool specifically, it takes precedence.
""")
@htmlize.register(bool)
def _(b: bool) -> str:
"""Specialized handler for booleans (registered AFTER int handler)"""
return f'<pre>{b}</pre>'
print("Testing boolean handler:")
print(f"htmlize(True) = {htmlize(True)}")
print(f"htmlize(False) = {htmlize(False)}")
print(f"htmlize(42) = {htmlize(42)}")
print("Note: bool handler takes precedence over int handler!")
# ============ EXAMPLE 5: Registering With Explicit Types
print("\n" + "=" * 70)
print("# ============ EXAMPLE 5: Registering With Explicit Types")
print("Alternative syntax using explicit type arguments:\n")
print("""
TWO WAYS TO REGISTER:
Method 1 (type annotation in function signature):
@htmlize.register(str)
def _(text: str) -> str:
...
Method 2 (explicit type as argument):
@htmlize.register(fractions.Fraction)
def _(x) -> str:
...
Both work! Use whichever is clearer.
""")
@htmlize.register(fractions.Fraction)
def _(x) -> str:
"""Specialized handler for fractions"""
frac = fractions.Fraction(x)
return f'<pre>{frac.numerator}/{frac.denominator}</pre>'
print("Testing fraction handler:")
result = htmlize(fractions.Fraction(2, 3))
print(f"htmlize(Fraction(2, 3)) = {result}")
# ============ EXAMPLE 6: Registering Multiple Types to Same Handler
print("\n" + "=" * 70)
print("# ============ EXAMPLE 6: Registering Multiple Types to Same Handler")
print("Decorating one handler for multiple types:\n")
print("""
Sometimes you want the SAME handler for multiple types.
You can stack @register decorators on a single function.
Example: float and decimal.Decimal should both show with fractions
""")
@htmlize.register(decimal.Decimal)
@htmlize.register(float)
def _(x) -> str:
"""Specialized handler for floats and decimals"""
frac = fractions.Fraction(x).limit_denominator()
return f'<pre>{x} ({frac.numerator}/{frac.denominator})</pre>'
print("Testing float and decimal handlers:")
print(f"htmlize(2/3) = {htmlize(2/3)}")
print(f"htmlize(0.6666666666666666) = {htmlize(0.6666666666666666)}")
print(f"htmlize(Decimal('0.02380952')) = {htmlize(decimal.Decimal('0.02380952'))}")
# ============ EXAMPLE 7: Complete htmlize Example
print("\n" + "=" * 70)
print("# ============ EXAMPLE 7: Complete htmlize Example")
print("Using the complete htmlize function:\n")
examples = [
42,
'Hello & goodbye',
['apple', 10, {3, 2, 1}],
True,
fractions.Fraction(2, 3),
2/3,
decimal.Decimal('0.02380952'),
{1, 2, 3},
]
print("Testing htmlize with various types:\n")
for obj in examples:
result = htmlize(obj)
print(f"htmlize({obj!r})")
print(f" -> {result}\n")
# ============ EXAMPLE 8: Understanding Dispatch Rules
print("\n" + "=" * 70)
print("# ============ EXAMPLE 8: Understanding Dispatch Rules")
print("How Python decides which handler to call:\n")
print("""
DISPATCH ALGORITHM:
1. Check the type of the first argument
2. If exact match in registered types, use that handler
3. Otherwise, check the MRO (Method Resolution Order)
4. Use the most specific handler in the MRO
5. If no match, use the @singledispatch base function
EXAMPLE MRO FOR bool:
bool -> int -> numbers.Integral -> numbers.Number -> object
If you register handlers for:
- bool: calls bool handler
- int: calls int handler (if bool not registered)
- numbers.Integral: calls Integral handler (if bool/int not registered)
- object: calls base handler (default)
WHY THIS MATTERS:
Register specific types AFTER general types. The most specific handler wins.
But the order of decorator application doesn't matter - dispatch is by type.
""")
# ============ EXAMPLE 9: Introspection - Exploring Registered Types
print("\n" + "=" * 70)
print("# ============ EXAMPLE 9: Introspection - Exploring Registered Types")
print("Discovering what types are registered:\n")
print("The dispatch registry is accessible as htmlize.registry:")
print(f"Type(s): {type(htmlize.registry)}")
print(f"Registered types: {list(htmlize.registry.keys())}")
print("\nFor each type, you can get the handler:")
print(f"Handler for str: {htmlize.register(str)}")
print(f"Handler for list: {htmlize.register(abc.Sequence)}")
print("\nCheck if a type has a handler:")
print(f"str in registry: {str in htmlize.registry}")
print(f"int in registry: {int in htmlize.registry}")
# ============ EXAMPLE 10: Real-World Use Case - Data Serialization
print("\n" + "=" * 70)
print("# ============ EXAMPLE 10: Real-World Use Case - Data Serialization")
print("Using singledispatch for type-based serialization:\n")
@singledispatch
def serialize(obj):
"""Serialize any Python object to a JSON-compatible format"""
raise TypeError(f"Cannot serialize {type(obj).__name__}")
@serialize.register(str)
def _(s: str):
return s
@serialize.register(int)
def _(i: int):
return i
@serialize.register(float)
def _(f: float):
return f
@serialize.register(list)
def _(lst: list):
return [serialize(item) for item in lst]
@serialize.register(dict)
def _(d: dict):
return {k: serialize(v) for k, v in d.items()}
@serialize.register(tuple)
def _(t: tuple):
return [serialize(item) for item in t]
print("Serialization examples:")
data = {
'name': 'Alice',
'age': 30,
'scores': [85.5, 92.0, 78.5],
'tags': ('python', 'programming'),
}
print(f"Original: {data}")
result = serialize(data)
print(f"Serialized: {result}")
# ============ EXAMPLE 11: When to Use Singledispatch
print("\n" + "=" * 70)
print("# ============ EXAMPLE 11: When to Use Singledispatch")
print("Best practices and patterns:\n")
print("""
WHEN TO USE @singledispatch:
✓ When you need type-based behavior variation
- Different processing for different types
- Format, convert, validate based on type
✓ When you want clean, extensible code
- Add new types without modifying existing code
- Each type handler is self-contained
- External code can register new types
✓ When you need to avoid if/elif chains
- More Pythonic and readable
- Type specialization is explicit
EXAMPLES:
- Format/serialize different types differently
- Visitor pattern without visitor classes
- Type-specific validation
- Rendering/display by type
- Database operations by type
WHEN NOT TO USE:
✗ When you need multiple dispatch (multiple argument types matter)
- Use multipledispatch library instead
- Or use method dispatch on an object
✗ When a simple isinstance check is sufficient
- If you only handle 2-3 types, if/else might be clearer
✗ When you need class method dispatch
- Use @functools.singledispatchmethod instead (Python 3.8+)
CAUTION: singledispatch dispatches on FIRST ARGUMENT ONLY
If you need to dispatch on other arguments, use multipledispatch or another pattern.
""")
# ============ EXAMPLE 12: Common Gotchas
print("\n" + "=" * 70)
print("# ============ EXAMPLE 12: Common Gotchas")
print("Things to watch out for:\n")
print("""
GOTCHA 1: Order of registration matters for subtypes
bool is a subclass of int. Register bool AFTER int for correct dispatch.
GOTCHA 2: Only the first argument is checked
@singledispatch only looks at type of first argument.
For other arguments, use isinstance checks inside the handler.
GOTCHA 3: Non-registered types don't error by default
They use the base @singledispatch function.
If you want strict typing, raise TypeError in the base function.
GOTCHA 4: Registering abstract types catches subtypes
@htmlize.register(abc.Sequence) catches list, tuple, str, etc.
This is powerful but can be surprising!
GOTCHA 5: Can't override a registered type at runtime
Once registered, the handler is locked in.
You can't change it or unregister it.
(Well, you can access registry.register() but it's not recommended)
""")
print("\n" + "=" * 70)
print("SUMMARY")
print("=" * 70)
print("""
KEY TAKEAWAYS:
1. @singledispatch ENABLES TYPE-BASED FUNCTION POLYMORPHISM
One generic function, multiple type-specific implementations.
2. BASIC PATTERN:
@singledispatch
def func(obj): ... # Generic implementation
@func.register(TypeA)
def _(obj: TypeA): ... # Type-specific handler A
@func.register(TypeB)
def _(obj: TypeB): ... # Type-specific handler B
3. DISPATCH RULES:
- Check exact type match first
- If not found, check MRO (Method Resolution Order)
- Use most specific match
- Fall back to base @singledispatch if no match
4. BENEFITS:
- Cleaner than if/elif chains
- Extensible - add types without modifying function
- Type specialization is explicit and clear
- Pythonic and elegant
5. IMPORTANT POINTS:
- Only first argument is checked
- All type handlers are separate functions
- Subtypes are handled via MRO
- Order matters for subtypes (e.g., bool vs int)
- Use @func.register(Type) or type annotations
6. PRACTICAL USES:
- Format/serialize by type
- Render/display by type
- Validate by type
- Process data differently per type
- Extensible visitor patterns
7. WHEN TO USE:
- Type-based behavior variation
- Want clean, extensible code
- Avoiding complex if/elif chains
""")
Runnable Example: function_registration_pattern.py¶
"""
TUTORIAL: Function Registration Pattern with Decorators
This tutorial covers the FUNCTION REGISTRATION PATTERN, a powerful way to use
decorators to dynamically build a registry of functions.
The Idea: You want to collect multiple functions into a central list or dict,
often for callbacks, plugins, or dispatch. Instead of manually maintaining the
registry, use a decorator to automatically register functions as they're defined.
The Pattern: When a function is decorated with @register, it's added to a
registry list/dict, then returned unchanged so it can still be called normally.
This is the foundation of plugin systems, event dispatchers, and dynamic dispatch
mechanisms. It's elegant because the registry is built naturally as the code runs.
"""
if __name__ == "__main__":
print("=" * 70)
print("TUTORIAL: Function Registration Pattern with Decorators")
print("=" * 70)
# ============ EXAMPLE 1: The Basic Registration Pattern
print("\n# ============ EXAMPLE 1: The Basic Registration Pattern")
print("The simplest registration decorator:\n")
# Create a registry list to hold functions
registry = []
def register(func):
"""Decorator that registers a function in the registry"""
print(f"running register({func.__name__})")
registry.append(func) # Add the function to the registry
return func # Return the function unchanged
print("Define the registry and register decorator:")
print(f"registry = {registry}")
print()
# Now define some functions using the @register decorator
@register
def f1():
"""First registered function"""
print("running f1()")
@register
def f2():
"""Second registered function"""
print("running f2()")
def f3():
"""Not registered - no @register decorator"""
print("running f3()")
print("\nFunctions have been defined. Check the registry:")
print(f"registry = {registry}")
print(f"registry[0].__name__ = {registry[0].__name__}")
print(f"registry[1].__name__ = {registry[1].__name__}")
print("\nNotice: f3 is NOT in the registry (no @register decorator)")
# ============ EXAMPLE 2: Using the Registered Functions
print("\n" + "=" * 70)
print("# ============ EXAMPLE 2: Using the Registered Functions")
print("Call registered and non-registered functions:\n")
print("Calling f1() directly:")
f1()
print("\nCalling f2() directly:")
f2()
print("\nCalling f3() directly (not registered):")
f3()
print("\nCalling all registered functions via the registry:")
for func in registry:
func()
# ============ EXAMPLE 3: Understanding How Registration Works
print("\n" + "=" * 70)
print("# ============ EXAMPLE 3: Understanding How Registration Works")
print("Step-by-step breakdown of what happens:\n")
print("""
WHEN YOU WRITE:
@register
def f1():
print('running f1()')
PYTHON DOES THIS:
1. Define the function f1
2. Call register(f1) <- decorator is applied
3. Assign the return value back to f1
INSIDE register():
1. func = f1 (the function object)
2. print(f'running register({func})') <- debug message
3. registry.append(func) <- ADD TO REGISTRY
4. return func <- return unchanged
RESULT:
- f1 is in the registry
- f1 variable still points to the function
- f1 can be called normally
- It's also in the registry for batch operations
""")
# ============ EXAMPLE 4: Registry as a Dictionary
print("\n" + "=" * 70)
print("# ============ EXAMPLE 4: Registry as a Dictionary")
print("Use a dict registry instead of list for named lookups:\n")
# Create a dict registry
command_registry = {}
def register_command(func):
"""Register a function as a command by its name"""
name = func.__name__
print(f"Registering command: {name}")
command_registry[name] = func
return func
@register_command
def save():
"""Save current data"""
print("Saving data...")
@register_command
def load():
"""Load data from disk"""
print("Loading data...")
@register_command
def exit_program():
"""Exit the program"""
print("Exiting...")
print("Registered commands:")
for name in command_registry:
print(f" {name}")
print("\nCall a command by name:")
command_registry['save']()
print("\nCall all commands:")
for name, cmd in command_registry.items():
print(f"Calling {name}():")
cmd()
# ============ EXAMPLE 5: Real-World Example - Event Dispatcher
print("\n" + "=" * 70)
print("# ============ EXAMPLE 5: Real-World Example - Event Dispatcher")
print("An event system where handlers register for specific events:\n")
class EventDispatcher:
"""Simple event dispatcher with registered handlers"""
def __init__(self):
self.handlers = {} # event_name -> list of handler functions
def register_handler(self, event_name):
"""Decorator to register a handler for an event"""
def decorator(func):
if event_name not in self.handlers:
self.handlers[event_name] = []
print(f"Registering {func.__name__} for event '{event_name}'")
self.handlers[event_name].append(func)
return func
return decorator
def emit(self, event_name, *args, **kwargs):
"""Trigger an event and call all registered handlers"""
if event_name not in self.handlers:
print(f"No handlers for event: {event_name}")
return
for handler in self.handlers[event_name]:
handler(*args, **kwargs)
# Create a dispatcher
dispatcher = EventDispatcher()
# Register handlers for 'user_login' event
@dispatcher.register_handler('user_login')
def log_login(username):
print(f" LOG: User {username} logged in")
@dispatcher.register_handler('user_login')
def send_welcome_email(username):
print(f" EMAIL: Sending welcome email to {username}")
@dispatcher.register_handler('user_login')
def update_last_seen(username):
print(f" DB: Updating last_seen for {username}")
# Register handlers for 'user_logout' event
@dispatcher.register_handler('user_logout')
def log_logout(username):
print(f" LOG: User {username} logged out")
print("\nDispatcher handlers registered:")
for event, handlers in dispatcher.handlers.items():
print(f" {event}: {len(handlers)} handlers")
print("\nEmitting 'user_login' event:")
dispatcher.emit('user_login', 'alice')
print("\nEmitting 'user_logout' event:")
dispatcher.emit('user_logout', 'alice')
# ============ EXAMPLE 6: Plugin System Pattern
print("\n" + "=" * 70)
print("# ============ EXAMPLE 6: Plugin System Pattern")
print("Using registration for a simple plugin system:\n")
class PluginRegistry:
"""Registry for plugins that process different file types"""
def __init__(self):
self.plugins = {} # file_extension -> processor function
def register(self, *extensions):
"""Decorator to register a plugin for file extensions"""
def decorator(func):
for ext in extensions:
print(f"Registering {func.__name__} for .{ext} files")
self.plugins[ext] = func
return func
return decorator
def process(self, filename):
"""Process a file using the appropriate plugin"""
# Extract file extension
if '.' not in filename:
print(f"No extension for file: {filename}")
return
ext = filename.split('.')[-1]
if ext not in self.plugins:
print(f"No processor for .{ext} files")
return
processor = self.plugins[ext]
processor(filename)
# Create a plugin registry
plugins = PluginRegistry()
@plugins.register('txt')
def process_text(filename):
print(f"Processing text file: {filename}")
@plugins.register('jpg', 'jpeg', 'png')
def process_image(filename):
print(f"Processing image file: {filename}")
@plugins.register('py')
def process_python(filename):
print(f"Processing Python file: {filename}")
print("\nRegistered file processors:")
for ext, processor in plugins.plugins.items():
print(f" .{ext} -> {processor.__name__}")
print("\nProcessing various files:")
plugins.process('document.txt')
plugins.process('photo.jpg')
plugins.process('script.py')
plugins.process('backup.zip') # No processor
# ============ EXAMPLE 7: Comparison - Manual vs Registration Pattern
print("\n" + "=" * 70)
print("# ============ EXAMPLE 7: Comparison - Manual vs Registration Pattern")
print("Why the registration pattern is better:\n")
print("""
MANUAL APPROACH (without registration):
def process_files(files):
processors = [
process_txt,
process_json,
process_csv,
# Must remember to add every processor here
# Hard to extend from other modules
]
for file in files:
# Check type and call processor
...
PROBLEMS:
- Central list must be manually maintained
- Hard to extend without modifying this function
- Couples all processors together
- External modules can't register processors
REGISTRATION PATTERN:
@register
def process_txt():
...
@register
def process_json():
...
# External module can do:
@register
def process_csv():
...
BENEFITS:
- Processors register themselves as they're defined
- Easy to extend - just add @register
- No central coupling
- Each processor is independent
- Works great with plugins from external modules
""")
# ============ EXAMPLE 8: Registration with Validation
print("\n" + "=" * 70)
print("# ============ EXAMPLE 8: Registration with Validation")
print("Add validation/requirements to registration:\n")
class ValidatedRegistry:
"""Registry that validates handlers before registration"""
def __init__(self):
self.handlers = {}
def register(self, **requirements):
"""
Decorator that registers with validation.
Usage: @register(priority=1, required=True)
"""
def decorator(func):
# Validate the function
if not callable(func):
raise ValueError(f"{func} is not callable")
name = func.__name__
meta = {'function': func, **requirements}
print(f"Registering {name} with {requirements}")
self.handlers[name] = meta
return func
return decorator
def call_handler(self, name, *args, **kwargs):
"""Call a registered handler by name"""
if name not in self.handlers:
raise KeyError(f"Handler {name} not registered")
handler = self.handlers[name]['function']
return handler(*args, **kwargs)
validated_registry = ValidatedRegistry()
@validated_registry.register(priority=1, required=True)
def database_check():
print(" Checking database...")
@validated_registry.register(priority=2)
def cache_check():
print(" Checking cache...")
print("\nRegistered handlers with metadata:")
for name, meta in validated_registry.handlers.items():
print(f" {name}: priority={meta.get('priority')}, "
f"required={meta.get('required')}")
print("\nCalling handlers:")
validated_registry.call_handler('database_check')
validated_registry.call_handler('cache_check')
# ============ EXAMPLE 9: Decorator Chaining
print("\n" + "=" * 70)
print("# ============ EXAMPLE 9: Decorator Chaining")
print("Combine registration with other decorators:\n")
import time
call_log = []
def log_calls(func):
"""Decorator that logs when a function is called"""
def wrapper(*args, **kwargs):
print(f" LOG: Calling {func.__name__}")
result = func(*args, **kwargs)
call_log.append(func.__name__)
return result
return wrapper
def time_calls(func):
"""Decorator that times function execution"""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f" TIME: {func.__name__} took {elapsed:.4f}s")
return result
return wrapper
timed_registry = []
def register_timed(func):
"""Register and apply timing decorator"""
print(f"Registering {func.__name__}")
timed_registry.append(func)
return func
@register_timed
@time_calls
@log_calls
def operation_a():
time.sleep(0.01)
print(" Operation A complete")
@register_timed
@time_calls
@log_calls
def operation_b():
time.sleep(0.02)
print(" Operation B complete")
print("\nRunning registered operations:")
for op in timed_registry:
op()
print(f"\nAll operations called: {call_log}")
# ============ EXAMPLE 10: Common Use Cases
print("\n" + "=" * 70)
print("# ============ EXAMPLE 10: Common Use Cases")
print("Where the registration pattern shines:\n")
print("""
COMMON USE CASES:
1. PLUGIN SYSTEMS
- Plugins register themselves at import time
- Main app calls all registered plugins
- Easy to add/remove plugins
2. EVENT SYSTEMS
- Register handlers for events
- When event occurs, call all handlers
- Multiple handlers per event
3. COMMAND DISPATCHERS
- Commands register by name
- Main loop dispatches to command by name
- Easy to add new commands
4. TESTING FRAMEWORKS
- Test functions register themselves
- Test runner discovers and runs all tests
- No need to manually list tests
5. API ENDPOINTS
- Routes register themselves as they're defined
- Server builds routing table automatically
- Similar to Flask @app.route()
6. PROTOCOL HANDLERS
- Different handlers for different data types
- Each handler registers for its type(s)
- Extensible dispatch system
7. MIDDLEWARE CHAINS
- Middleware registers itself in order
- Request passes through all middleware
- Easy to add/remove middleware
KEY PATTERN:
1. Create a registry (list or dict)
2. Define a register decorator that:
- Adds function to registry
- Optionally validates
- Returns function unchanged
3. Use @register on functions you want to collect
4. Execute or dispatch to registered functions as needed
""")
# ============ EXAMPLE 11: Best Practices
print("\n" + "=" * 70)
print("# ============ EXAMPLE 11: Best Practices")
print("Guidelines for using the registration pattern:\n")
print("""
BEST PRACTICES:
1. ALWAYS RETURN THE FUNCTION
@decorator should return the original function unchanged
This way it can still be called and used normally
2. DOCUMENT REGISTRATION REQUIREMENTS
Make clear what a function must do to be registered
Validate inputs and raise clear errors
3. CONSIDER NAMING CONVENTIONS
Functions registered the same way often have similar names
Use decorators to discover and group related functions
4. PROVIDE INTROSPECTION
Make it easy to see what's registered
offer ways to query the registry
5. HANDLE REGISTRATION CONFLICTS
What happens if two functions have the same name/key?
Should you error, warn, or replace?
Make policy clear
6. USE DECORATOR FACTORIES FOR PARAMETERS
If decorator needs parameters, use a factory:
@register(name='custom_name') <- factory
def func(): ...
7. KEEP DECORATORS FOCUSED
Register decorator should only register
Don't mix in validation, timing, etc.
(Or do, but document it clearly)
8. MAKE REGISTRATION EXPLICIT
Use a clear name like @register, not @dec
Makes it obvious what the decorator does
ANTI-PATTERNS:
X Don't lose the original function
Don't do: register(func) then ignore return value
X Don't silently fail
Raise clear errors if registration fails
X Don't make registry hard to inspect
Expose it for debugging and introspection
X Don't mix registration with side effects
If decorator does registration, make that clear
""")
print("\n" + "=" * 70)
print("SUMMARY")
print("=" * 70)
print("""
KEY TAKEAWAYS:
1. REGISTRATION PATTERN BASICS
- Create a registry (list or dict)
- Create a @register decorator
- Decorator adds function to registry and returns it
- Use @register on functions you want to collect
2. BASIC PATTERN:
registry = []
def register(func):
registry.append(func)
return func
@register
def my_function():
...
3. COMMON VARIATIONS
- Dict registry for named lookup: registry[name] = func
- Decorator factory for parameters: @register(priority=1)
- Validation: check requirements before registering
- Metadata: attach extra info to registered functions
4. BENEFITS
- Automatic collection of functions
- Easy to extend without modifying core code
- Elegant way to build dispatch systems
- Works great with plugins
5. COMMON USES
- Event systems and callbacks
- Plugin architectures
- Command dispatchers
- API routing
- Test discovery
- Middleware chains
6. KEY PRINCIPLE
"Register functions as they're defined, not in a central list"
This makes code more decoupled and extensible.
7. REMEMBER
- Always return the function unchanged
- Keep decorator focused
- Document registration requirements
- Make registry inspectable
- Handle conflicts clearly
""")