Перейти к содержанию

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

  1. 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.
  2. Use the pattern when it’s awkward or not possible to extend an object’s behavior using inheritance.

Basics

  1. Python’s functions are objects. Therefore, functions:
    1. can be assigned to a variable;
    2. can be defined inside another function;
    3. 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:

@add_some_new_behavior_to_the_function
def unmodified_decorated_function():
    pass

@decorator is just a shortcut to:

unmodified_decorated_function = add_some_new_behavior_to_the_function(unmodified_decorated_function)

You can accumulate decorators:

  1. Like this:
    unmodified_decorated_function = add_some_new_behavior_to_the_function(
        add_some_more_new_behavior_to_the_function(
            unmodified_decorated_function
        )
    )
    
  2. Or like this:
    @add_some_new_behavior_to_the_function
    @add_some_more_new_behavior_to_the_function
    def unmodified_decorated_function():
        pass
    

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