Decorator
Definition
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
A decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.
Note
Which means that they let you execute code before and after the function they decorate without modifying the function itself.
Also known as Wrapper.
Applicability
- Use the Decorator pattern when you need to be able to assign extra behaviors to objects at runtime without breaking the code that uses these objects.
- Use the pattern when it’s awkward or not possible to extend an object’s behavior using inheritance.
Basics
- Python’s functions are objects. Therefore, functions:
- can be assigned to a variable;
- can be defined inside another function;
- can be passed as an argument to another function.
Success
That means that a function can return
another function.
How to create a decorator in Python
Below is an example of a function with two decorators applied to it. The order of execution of commands in the output of the function has been analyzed.
First decorator:
import functools
from typing import Callable, Any
# A decorator is a function that expects ANOTHER function as parameter.
def bar(func: Callable) -> Callable:
# Decorators are ORDINARY functions.
print('3. bar: This line will be executed before wrapper is called.')
# Inside, the decorator defines a function on the fly: the wrapper.
# This function is going to be wrapped around the original function
# so it can execute code before and after it.
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
# Put here the code you want to be executed BEFORE the original
# function is called.
print('6. bar.wrapper: Before the decorated function runs.')
# Call the function here (using parentheses).
func_returning = func(*args, **kwargs)
# Put here the code you want to be executed AFTER the original
# function is called.
print('10. bar.wrapper: After the decorated function runs.')
# Returns the result of the function being decorated.
return func_returning
print('4. bar: This line will ALSO be executed BEFORE wrapper is called.')
# At this point, `func` HAS NEVER BEEN EXECUTED.
# We return the wrapper function we have just created.
# The wrapper contains the function and the code to execute before
# and after. It’s ready to use!
return wrapper
Second decorator:
def baz(func: Callable) -> Callable:
print('1. baz: This line will be executed before wrapper is called.')
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print('7. baz.wrapper: Before the decorated function runs.')
func_returning = func(*args, **kwargs)
print('9. baz.wrapper: After the decorated function runs.')
return func_returning
print('2. baz: This line will ALSO be executed BEFORE wrapper is called.')
return wrapper
Function with decorators applied:
# The order you set the decorators matters.
@bar # Equivalent to `foo = bar(foo)`.
@baz # Equivalent to `foo = bar(baz(foo))`
def foo(func_arg: str) -> str:
print('8. foo: I am a function that cannot be modified.')
return func_arg
Warning
The order you set the decorators matters.
Execution and output:
print(f'5. Correct `foo.__name__` because `@functools.wraps(func)` is used: {foo.__name__}.')
foo_returning = foo(func_arg='I am the result of the function being decorated.')
print(f'11. foo: {foo_returning}')
# Outputs:
# 1. baz: This line will be executed before wrapper is called.
# 2. baz: This line will ALSO be executed BEFORE wrapper is called.
# 3. bar: This line will be executed before wrapper is called.
# 4. bar: This line will ALSO be executed BEFORE wrapper is called.
# 5. Correct `foo.__name__` because `@functools.wraps(func)` is used: foo.
# 6. bar.wrapper: Before the decorated function runs.
# 7. baz.wrapper: Before the decorated function runs.
# 8. foo: I am a function that cannot be modified.
# 9. baz.wrapper: After the decorated function runs.
# 10. bar.wrapper: After the decorated function runs.
# 11. foo: I am the result of the function being decorated.
How to apply a decorator in Python
You can do this:
@decorator
is just a shortcut to:
unmodified_decorated_function = add_some_new_behavior_to_the_function(unmodified_decorated_function)
You can accumulate decorators:
- Like this:
- Or like this:
Decorator for a class method
Notice that the decorator type annotation that mypy
accepts has been added.
import functools
from typing import Any, Callable, TypeVar, cast
TFun = TypeVar('TFun', bound=Callable[..., Any])
def foo(func: TFun) -> TFun:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return func(*args, **kwargs)
return cast(TFun, wrapper)
class MyCar(object):
def __init__(self) -> None:
self.total_number_of_passengers = 4
@foo
def add_passengers(self, num_of_passengers_to_add: int) -> None:
self.total_number_of_passengers += num_of_passengers_to_add
my_car = MyCar()
my_car.add_passengers(num_of_passengers_to_add=1)
print(f'total_number_of_passengers: {my_car.total_number_of_passengers}')
# Outputs:
# total_number_of_passengers: 5