05 Functions - 08 Decorator Functions
A decorator accepts a function as an argument, modifies its behavior, and returns a new function.
A decorator is a special type of function in Python that allows you to modify or enhance the behavior of another function without changing the function’s code.
Decorators are widely used in Python to add additional functionality to functions or methods, such as logging, access control, performance measurement, and more.
Basic Decorator Structure
To create a decorator, we need to define a function that accepts another function (func) as its argument. Inside the decorator, we define another function (often referred to as inner or wrapper) that will modify the behavior of the original function and return the modified result.
Here’s the basic structure of a decorator:
# A decorator is a special type of function that modifies another function's behavior
>>> def decor(func):
... def inner():
... value = func() # Call func and get the result
... return value + 2 # Modify the result and return it
... return inner # Return the inner functiondecor(func): The decorator function that accepts another function as its argument.inner(): The inner function that modifies the result of the passed function (func) and returns the modified result.return inner: The decorator function returns theinnerfunction, which is now the decorated version offunc.
Applying a Decorator to a Function
Once we’ve defined the decorator, we can apply it to any function by passing the function to the decorator. We can use the decorator like this:
>>> def decor(func):
... def inner():
... value = func() # Call func and get value
... return value + 2 # Modify the result and return it
... return inner # Return the inner function
>>>
>>> def num():
... return 10
>>>
>>> result_func = decor(num) # Apply the decorator to the function
>>> print(result_func()) # Call the decorated function
12- The
num()function returns10. - The
decor()function adds2to the result ofnum()and returns12. - The
inner()function returned bydecor()is assigned toresult_func, and callingresult_func()gives the modified result.
Using the @ Symbol to Apply Decorators
The @ symbol is a shorthand for applying a decorator to a function. Using @decor directly above the function definition allows Python to automatically apply the decorator to the function without needing to explicitly call the decorator.
>>> @decor
>>> def num():
... return 10
>>>
>>> print(num())
12
# The result will be 12 because the decorator adds 2 to the return valueThis is equivalent to:
>>> def num():
... return 10
>>>
>>> num = decor(num)Now, when we call num(), the decorator decor will automatically be applied to modify the result:
Chaining Multiple Decorators
You can apply multiple decorators to a single function. When multiple decorators are applied, they are executed from the bottom up, i.e., the decorator closest to the function definition is executed first, and then the others are applied in order.
>>> def decor1(func):
... def inner():
... value = func() # Call func and get value
... return value + 2 # Modify the result and return it
... return inner # Return the inner function
>>>
>>> def decor2(func):
... def inner():
... value = func() # Call func and get value
... return value * 2 # Modify the result and return it
... return inner # Return the inner function
>>>
>>> @decor1
>>> @decor2
>>> def num():
... return 10
>>>
>>> print(num()) # Output: 24
24In this example:
- The
num()function returns10. decor2multiplies the result by2, making it20.decor1adds2, resulting in24.
Without the @ syntax, this would be written as:
>>> result_func = decor1(decor2(num))
>>> print(result_func())
24@decor2is applied first (it modifies the result ofnum()).- Then,
@decor1is applied to the result ofdecor2(num).
General Syntax for Chaining Multiple Decorators
When applying multiple decorators to a function, the syntax is as follows:
>>> @dec1
>>> @dec2
>>> def func(arg1, arg2, ...):
... passThis is equivalent to:
>>> def func(arg1, arg2, ...):
... pass
>>> func = dec1(dec2(func))In this case:
dec2is applied first, thendec1is applied to the result ofdec2(func).
More Complex Decorators
Example using decorators to add logging and timing functionality to a function:
>>> import time
>>>
>>> def log(func):
... def wrapper(*args, **kwargs):
... print(f"Calling function: {func.__name__} with arguments {args} and keyword arguments {kwargs}")
... return func(*args, **kwargs)
... return wrapper
>>>
>>> def timing(func):
... def wrapper(*args, **kwargs):
... start_time = time.time()
... result = func(*args, **kwargs)
... end_time = time.time()
... print(f"Execution time: {end_time - start_time} seconds")
... return result
... return wrapper
>>>
>>> @log
>>> @timing
>>> def calculate_sum(a, b):
... return a + b
>>>
>>> result = calculate_sum(5, 3)
>>> print(f"Result: {result}")
Calling function: calculate_sum with arguments (5, 3) and keyword arguments {}
Execution time: 0.000001 seconds
Result: 8@logwill log the function name and arguments whencalculate_sum()is called.@timingwill measure the execution time ofcalculate_sum().- The decorators are applied in a chain, starting with
@timing(which measures time) and then@log(which logs the function call).