This chapter introduces Decorators, a powerful and elegant Python feature that allows you to modify or enhance a function or class without explicitly changing its source code. This requires understanding functions as first-class citizens.
A Decorator is a function that takes another function as an argument, adds some kind of functionality, and then returns the wrapped function. They are syntactically represented by the @ symbol placed immediately before the function definition.
1. Prerequisite Concepts
To understand decorators, you must first understand how Python handles functions.
A. First-Class Functions
In Python, functions are first-class citizens, meaning they can be treated like any other variable or object:
- They can be passed as arguments to other functions.
- They can be returned as the result of other functions.
- They can be assigned to variables.
B. Inner Functions (Closures)
A Closure is a function defined inside another function (an inner function) that remembers and accesses variables from the scope of the outer function, even after the outer function has finished executing.
def outer_function(msg):
# 'msg' is a free variable captured by the inner function
def inner_function():
print(msg) # Inner function uses 'msg' from outer scope
return inner_function # Return the inner function object
# Create a new function object, 'hello_func', which is the inner_function.
hello_func = outer_function("Hello!")
# The inner function still remembers "Hello!" even though outer_function has finished.
hello_func()
# Output: Hello!
2. Defining a Decorator
A typical decorator function takes the function to be decorated (func) as input and defines an inner function (wrapper) that performs the desired actions before and/or after calling the original function.
def simple_decorator(func):
def wrapper():
print("--- STARTING FUNCTION ---") # Action BEFORE original function
func() # Call the original function
print("--- FUNCTION FINISHED ---") # Action AFTER original function
return wrapper
3. Applying a Decorator (@ Syntax)
The @ symbol is just syntactic sugar for a specific assignment operation:
@simple_decorator above a function is equivalent to: say_hello = simple_decorator(say_hello)
@simple_decorator
def say_hello():
print("Hello from the actual function!")
# When you call the decorated function, you are actually calling 'wrapper'
say_hello()
# Output:
# --- STARTING FUNCTION ---
# Hello from the actual function!
# --- FUNCTION FINISHED ---
4. Decorators with Arguments
To pass arguments to the decorated function, the inner wrapper function must accept arbitrary positional (*args) and keyword arguments (**kwargs) and pass them along to the original function.
def log_execution(func):
def wrapper(*args, **kwargs):
print(f"Executing {func.__name__} with args: {args} and kwargs: {kwargs}")
result = func(*args, **kwargs) # Pass arguments to the original function
print(f"{func.__name__} finished. Result: {result}")
return result
return wrapper
@log_execution
def power(a, b=2):
return a ** b
power(5, b=3)
# Output:
# Executing power with args: (5,) and kwargs: {'b': 3}
# power finished. Result: 125
5. Practical Use Cases
Decorators are essential in professional Python code for tasks such as:
- Logging: Automatically logging function calls, inputs, and outputs. (See example above).
- Timing: Measuring how long a function takes to execute (performance testing).
- Caching/Memoization: Storing the results of expensive function calls to avoid recalculation.
- Authentication/Authorization: Restricting access to a function based on user permissions (common in web frameworks like Flask/Django).
