Декораторы функций

Декораторы — это, своеобразные "функции-обёртки", которые дают нам возможность делать что-либо до и после того, что сделает декорируемая функция, не изменяя код базой декорируемой функции.

Понять как устроен и работает декоратор можно из примера:

def set_first_decor(fn):
    def wrapped():
        text = fn()
        print(f'1. Set upper text: {text} to {text.upper()}')
        return text.upper()
    return wrapped


def set_second_decor(fn):
    def wrapped():
        text = fn()
        print(f'2. Change text: {text} to {text[:-3]}!!!')
        return f"{text[:-3]}!!!"
    return wrapped


@set_second_decor
@set_first_decor
def print_hello():
    return "hello decorator???"


print(print_hello())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Результат выполнения:

1. Set upper text: hello decorator??? to HELLO DECORATOR???
2. Change text: HELLO DECORATOR??? to HELLO DECORATOR!!!
HELLO DECORATOR!!!
1
2
3

... если понять не удалось, начнем разбираться.

Python функции - объекты

Чтобы понять, как работают декораторы, нужно осознать, что в Python функции — это объекты. Давайте посмотрим, что из этого следует:

def up_first_letter(line):
    return line.capitalize()+"!"

print(up_first_letter('hello'))     # Hi!
1
2
3
4

Так как функция - это объект, можем связать её с переменной:

string_up = up_first_letter
1

При этом не используем скобок, т.к. не вызываем функцию up_first_letter, а связываем её с переменной string_up. Это означает, что теперь мы можем вызывать функцию up_first_letter через переменную string_up:

def up_first_letter(line):
    return line.capitalize()+"!"

string_up = up_first_letter
print(string_up('hi again'))      # Hi again!
1
2
3
4
5

При этом можем даже удалить функцию up_first_letter, но функция всё ещё будет доступна через переменную string_up:

def up_first_letter(line):
    return line.capitalize()+"!"

string_up = up_first_letter
del up_first_letter
try:
    print(up_first_letter(''))
except NameError as e:
    print("type error:", str(e))
    # type error: name 'up_first_letter' is not defined

print(string_up('still work'))      # Still work!
1
2
3
4
5
6
7
8
9
10
11
12

Функция в функции

Функция в Python может быть определена внутри другой функции и вызвана. Тогда при вызове функции outside внутри нее определяется, а затем вызывается функция inside:

def outside():
    def inside(text):
        return text.upper()+"..."

    print(inside('works inside'))

outside()   # WORKS INSIDE...
1
2
3
4
5
6
7

Но вне функции outside функцию inside вызвать нельзя, т.к. внутренняя функция находиться в локальном пространстве имен внешней функции outside:

def outside():
    def inside(text):
        return text.upper()+"..."

    print(inside('works inside'))

try:
    print(inside('try'))
except NameError as e:
    print("type error:", str(e))
    # type error: name 'inside' is not defined
1
2
3
4
5
6
7
8
9
10
11

Ссылки на функции

Функции являются полноправными объектами, а значит:

  • могут быть связаны с переменной;
  • могут быть определены одна внутри другой.

Внешняя функция может возвращать результат работы внутренней функции:

def outside():
    def inside(text):
        return text.upper()+"!"
    return inside('return: works inside')

any_function = outside
print(any_function())   # RETURN: WORKS INSIDE!



 



1
2
3
4
5
6
7

Но, если нам нужен не результат работы внутренней функции, а возможность работы с внутренней функции из вне. Тогда нужно вернуть внутреннюю функцию, без вызова (т.е. обратившись по имени, но без скобок):

def outside():
    def inside(text):
        return text.upper()+"!"
    return inside

any_function = outside()
print(any_function('outside text'))   # OUTSIDE TEXT!

# Теперь внутреннюю функцию можно вызвать напрямую:
print(outside()('it works'))            # IT WORKS!



 






1
2
3
4
5
6
7
8
9
10

Для лучшего понимания постарайтесь понять работу следующего кода, как именно происходить вызов функций и их выполнение:

def run_function(number=0):
    def run_first(text='first'):
        return text.capitalize()+'!'

    def run_second(text='second'):
        return text.upper()+'!!!'

    if number == 1:
        return run_first
    else:
        return run_second

test = run_function()
print(test())               # SECOND!!!
print(test('try'))          # TRY!!!

test = run_function(1)
print(test())               # First!
print(test('try 1'))        # Try 1!

test = run_function
print(test(2)())            # SECOND!!!
print(test(2)('try 2'))     # TRY 2!!!

print(run_function(1)())            # First!
print(run_function()('try right'))  # TRY RIGHT!!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

Если можем возвращать функцию, значит, можем и передавать функцию другой функции, как параметр:

def run_print():
    print("Print any text...")

def run_before_and_after(func):
    print("Do something before function")
    func()
    print("Do something after function")

run_before_and_after(run_print)
1
2
3
4
5
6
7
8
9

Результат выполнения:

Do something before function
Print any text...
Do something after function
1
2
3

Теперь у нас есть все необходимые знания для того, чтобы понять, как работают декораторы.

Декораторы — это, своеобразные "функции-обёртки", которые дают нам возможность делать что-либо до и после того, что сделает декорируемая функция, не изменяя код базой декорируемой функции.

Создадим декоратор «вручную»

Декоратор - это функция, ожидающая другую функцию в качестве параметра. Внутри себя декоратор определяет "функцию-обёртку", которая дает возможность выполнять произвольный код до и/или после переданной декоратору функции.

Создадим пример "неизменяемой" функции и вызовем её для понимания работы:

def immutable_function():
    print("It's immutable  function code")

immutable_function()    # It's immutable  function code
1
2
3
4

Далее допишем свой декоратор и приметим его к "неизменяемой" функции:

def apply_decorator(func):
    def wrapper():
        print("Do something before function")
        func()
        print("Do something after function")

    return wrapper

def immutable_function():
    print("It's immutable  function code")

immutable_function_decorated = apply_decorator(immutable_function)
immutable_function_decorated()
print("-"*30)
immutable_function()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Результат выполнения:

Do something before function
It's immutable  function code
Do something after function
------------------------------
It's immutable  function code
1
2
3
4
5

Создав переменную immutable_function_decorated и присвоив ей значение декоратора с переданной функцией apply_decorator(immutable_function) у далось добиться внешнего изменения поведения "неизменяемой" функции, но при прямом вызове самой базовой функции она работает по прежнему. Для того чтобы подменить вызов базовой функции нужно, чтобы имя переменной связывающей декоратор с изменяемой функцией совпадали. Т.е. по сути происходит перезапись функции immutable_function:

def apply_decorator(func):
    def wrapper():
        print("Do something before function")
        func()
        print("Do something after function")

    return wrapper

def immutable_function():
    print("It's immutable  function code")

print("Before decorating:")
immutable_function()
print("-"*30)

print("After decorating:")
immutable_function = apply_decorator(immutable_function)
immutable_function()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Результат выполнения:

Before decorating:
It's immutable  function code
------------------------------
After decorating:
Do something before function
It's immutable  function code
Do something after function
1
2
3
4
5
6
7

Изменим строки кода:

...
def immutable_function():
    print("It's immutable  function code")

immutable_function = apply_decorator(immutable_function)
immutable_function()
1
2
3
4
5
6

на строки кода:

@apply_decorator
def immutable_function():
    print("It's immutable  function code")

immutable_function()
1
2
3
4
5

Итоговый код примера:

def apply_decorator(func):
    def wrapper():
        print("Do something before function")
        func()
        print("Do something after function")

    return wrapper

@apply_decorator
def immutable_function():
    print("It's immutable  function code")

immutable_function()
1
2
3
4
5
6
7
8
9
10
11
12
13

И результат работы:

Do something before function
It's immutable  function code
Do something after function
1
2
3

@decorator — просто синтаксический сахар для конструкций вида:

immutable_function = apply_decorator(immutable_function)
1

Декораторы — это python-реализация паттерна проектирования «Декоратор».

Пример применения декоратора

Cоздадим декоратор, замеряющий время выполнения функции. Далее используем его на функции, которая делает GET-запрос к главной странице Google. Чтобы измерить скорость, мы сначала сохраняем время перед выполнением обёрнутой функции, выполняем её, снова сохраняем текущее время и вычитаем из него начальное:

import time
import requests

def benchmark(func):
    def wrapper():
        start = time.time()
        func()
        end = time.time()
        print(f'Lead time: {end - start} sec.')
    return wrapper

@benchmark
def get_webpage():
    requests.get('https://google.com')

get_webpage()   # Lead time: 0.35700130462646484 sec.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

TIP

Декораторы расширяют возможности функции без редактирования её кода и являются гибким инструментом для изменения чего угодно.