Decorators
Decorators allow you to modify or extend the behavior of a function or class without changing its original code.
They are widely used in real-world applications such as:
- Logging
- Authentication
- Caching
- Validation
- Performance tracking
Mental Model
Section titled “Mental Model”Think of a decorator like a wrapper around a function.
graph TD
A[Call Function] --> B[Decorator Wrapper]
B --> C[Run Code Before]
C --> D[Original Function]
D --> E[Run Code After]
E --> F[Return Result]
Basic Idea
Section titled “Basic Idea”A decorator is just:
- A function
- That takes another function
- Returns a new function
Step-by-step structure
Section titled “Step-by-step structure”def decorator(func): def wrapper(): # before func() # after return wrapperBasic Example
Section titled “Basic Example”import functools
def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print("Before function runs") result = func(*args, **kwargs) print("After function runs") return result
return wrapper
@my_decoratordef greet(): print("Hello!")
greet()What actually happens
Section titled “What actually happens”greet = my_decorator(greet)Why functools.wraps is Important
Section titled “Why functools.wraps is Important”Without wraps, important details are lost:
- Function name
- Docstring
- Help info
import functools
def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
@my_decoratordef greet(): """This function greets the user""" print("Hello")
print(greet.__name__) # greetprint(greet.__doc__) # This function greets the userHandling Arguments Properly
Section titled “Handling Arguments Properly”Decorators should work with any function signature.
That is why we use:
*args # positional arguments**kwargs # keyword argumentsExample
Section titled “Example”import functools
def log_calls(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"{func.__name__} finished") return result
return wrapper
@log_callsdef add(a, b): return a + b
print(add(2, 3))Decorators with Arguments
Section titled “Decorators with Arguments”Sometimes you want to configure a decorator.
For that, you need one more layer.
Structure
Section titled “Structure”def decorator_with_args(config): def decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decoratorExample: Repeat Execution
Section titled “Example: Repeat Execution”import functools
def repeat(times): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = None for _ in range(times): result = func(*args, **kwargs) return result
return wrapper
return decorator
@repeat(3)def greet(name): print(f"Hello {name}")
greet("Asha")Class Decorators
Section titled “Class Decorators”Decorators also work on classes.
They receive the class and can modify it.
def add_status(cls): cls.status = "ready" return cls
@add_statusclass Task: pass
print(Task.status) # readyReal-World Use Cases
Section titled “Real-World Use Cases”Common uses:
- Logging function calls
- Checking permissions
- Measuring execution time
- Caching results
- Validating inputs
Example: Timing a Function
Section titled “Example: Timing a Function”import functoolsimport time
def time_it(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds") return result
return wrapper
@time_itdef slow_function(): time.sleep(1)
slow_function()