Before we move on to decorators, we need to introduce two concepts — higher-order functions and function closures. What follows is a bit of theory; it isn't hard, and understanding it will let you work with decorators.
Higher-order functions
A higher-order function in programming is a function that takes other functions as arguments or returns another function as its result.
The core idea is that functions have the same status as any other data object.
We can pass functions as parameters:
def my_func(inside_func):
...
inside_func() # Вызов функции принятой в качестве аргумента
...And we can return a function as a result:
def a():
def b():
pass
return bTo better grasp higher-order functions, picture an assembly line that builds other robots — one machine builds another machine.
Let's make a function that runs the function it receives twice:
def twice_func(inside_func):
"""Функция, выполняющая дважды функцию принятую в качестве аргумента"""
inside_func()
inside_func()
def hello():
print("Hello")
test = twice_func(hello)
# Hello
# HelloWe can see that, having passed the hello function as an argument to twice_func, it was executed twice inside it. Now let's move on to function closures and figure out what they are.
Function closures
A closure in programming is a function whose body contains references to variables that are declared outside its body, in the surrounding code, and are not its arguments.
Recall the non-local (nonlocal) scope — that is the principle function closures rely on.
Let's make a function that returns a function which always adds one and the same number x:
def make_adder(x):
def adder(n):
return x + n # захват переменной "x" из nonlocal области
return adder # возвращение функции в качестве результатаIn other words, we built an assembly line that adds a fixed number x to any number n (see higher-order functions). So let's make a function that adds 5 to any number.
# функция, которая будет к любому числу прибавлять пятёрку
add_5 = make_adder(5)
print(add_5(10)) # 15
print(add_5(100)) # 105Having defined higher-order functions and function closures, let's move on to decorators, which are built upon them.
Decorators
Decorators are meant to attach any additional behavior to a main function — called the decorated function — that can run before, after, or even instead of the main function. Meanwhile, the source code of the decorated function is not touched at all.
The additional behavior might be measuring the function's execution time or checking extra conditions that allow the given function to run.

The figure above shows the simplest decorator, which performs no additional actions.
The decorated function is passed to the my_decorator decorator as an argument, thanks to higher-order functions. The decorated function is then passed via a closure into the wrapping function wrapper. And it is this wrapping function — which is exactly what can add extra behavior to the main one — that the decorator itself returns.
Let's look at an example of how to add extra behavior to a main function.
def my_decorator(a_function_to_decorate):
# Здесь мы определяем новую функцию - «обертку». Она нам нужна, чтобы выполнять
# каждый раз при вызове оригинальной функции, а не только один раз
def wrapper():
# здесь поместим код, которые будет выполняться до вызова, потом вызов
# оригинальной функции, потом код после вызова
print("Я буду выполнен до основного вызова!")
result = a_function_to_decorate() # не забываем вернуть значение исходной функции
print("Я буду выполнен после основного вызова!")
return result
return wrapperOur decorator will print the line «Я буду выполнен до основного вызова!» before the main call, and the line «Я буду выполнен после основного вызова!» respectively after the main call. If needed, we don't forget to return the result of the original function.
def my_function():
print("Я - оборачиваемая функция!")
return 0
print(my_function())
# Я - оборачиваемая функция!
# 0
decorated_function = my_decorator(my_function) # декорирование функции
print(decorated_function())
# Я буду выполнен до основного вызова!
# Я - оборачиваемая функция!
# Я буду выполнен после основного вызова!
# 0We can see that, having decorated my_function, we added new functionality to it without changing the source code of the function itself.
Why is this useful? For example, decorators can measure a function's execution time or the number of times a particular function is called; they can also store the results of computations (caching).
Let's try to measure the execution time of the built-in exponentiation to the power of 2 and of the corresponding operator.
import time
def decorator_time(fn):
def wrapper():
print(f"Запустилась функция {fn}")
t0 = time.time()
result = fn()
dt = time.time() - t0
print(f"Функция выполнилась. Время: {dt:.10f}")
return dt # задекорированная функция будет возвращать время работы
return wrapper
def pow_2():
return 10000000 ** 2
def in_build_pow():
return pow(10000000, 2)
pow_2 = decorator_time(pow_2)
in_build_pow = decorator_time(in_build_pow)
pow_2()
# Запустилась функция <function pow_2 at 0x7f938401b158>
# Функция выполнилась. Время: 0.0000011921
in_build_pow()
# Запустилась функция <function in_build_pow at 0x7f938401b620>
# Функция выполнилась. Время: 0.0000021458To measure the time we'll use the time module, which has a time() function that returns the current time. Knowing the time just before execution and immediately after, we can compute how long the function ran.
We can see that our function runs faster, but a single run is not conclusive. You need to perform a series of runs and find the average time — only then can you draw any conclusions.
Syntactic sugar
Decorators are such a frequently used construct in Python that they were given dedicated «syntactic sugar».
Syntactic sugar in a programming language means syntax features whose use does not affect a program's behavior but makes the language more convenient for humans to use.
You can use it like this:
@my_decorator
def my_function():
passThe exact same thing happens under the hood.
my_function = my_decorator(my_function)`@my_decorator` above a function is exactly the same as `my_function = my_decorator(my_function)`.
Keep in mind that when you use the syntactic sugar, the decorated function takes the place of the original function!
def my_decorator(fn):
def wrapper():
fn()
return wrapper # возвращается задекорированная функция, которая заменяет исходную
# выведем незадекорированную функцию
def my_function():
pass
print(my_function) # <function my_function at 0x7f938401ba60>
# выведем задекорированную функцию
@my_decorator
def my_function():
pass
print(my_function) # <function my_decorator.<locals>.wrapper at 0x7f93837059d8>We can see that after decoration, the original function's name refers not to the function itself but to the function that lived inside the decorator — in this case the wrapper function.
Passing arguments to the decorated function
Until now we've decorated only functions without arguments. But what happens if we try to decorate a function that takes arguments?
def do_it_twice(func):
def wrapper():
func(arg)
func(arg)
return wrapper
@do_it_twice
def say_word(word):
print(word)
say_word("Oo!!!")Running this code produces the following error:
TypeError: wrapper() takes 0 positional arguments but 1 was givenIt happens because the decorated wrapper function takes no arguments, unlike the original. Namely, the decorated function now hides behind the variable name say_word. And every subsequent call to say_word is a call to wrapper. Therefore, wrapper must be able to accept the same arguments as the original function and pass them into it. To avoid worrying about the number of arguments and to make our decorator universal, we'll use *args and **kwargs.
# декоратор, в котором встроенная функция умеет принимать аргументы
def do_it_twice(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
@do_it_twice
def say_word(word):
print(word)
say_word("Oo!!!")
# Oo!!!
# Oo!!!Let's sum up the key points about decorators:
Decorators add extra behavior to a function without changing its source code.
Decorators are calls to additional functions, so they slow your code down a little.
To pass arguments to the decorated function, use *args and **kwargs.
Here is a universal template for a decorator:
def my_decorator(fn):
print("Этот код будет выведен один раз в момент декорирования функции")
def wrapper(*args, **kwargs):
print('Этот код будет выполняться перед каждым вызовом функции')
result = fn(*args, **kwargs)
print('Этот код будет выполняться после каждого вызова функции')
return result
return wrapper