На каждом собеседовании спрашивают про декораторы. Что это? В чём прикол?

Олег

16-й уровень

Скорее всего Вы уже видели декораторы в коде python — это те строки, что начинаются с @. Декораторы позволяют динамически изменять поведение или расширять функциональность существующих функций без изменения самой функции.

@my_decorator
def func(arg):
    # make some magic with arg
    return 'hello'

Декораторы в python используют замыкания, простейший пример которого выглядит вот так:

def make_printer(string):
  def inner():
    print(string)
  return inner

>>> make_hello = make_printer('hello!')
>>> make_hello()
>>> hello!

В этом примере make_printer создаёт функцию, которая выводит строку. Новой функции мы присвоили переменную make_hello. Внутренняя фукнция inner является замыканием.

Попробуем написать простейшую функцию, которая выводит текст, который ей передали:

def print_text(string):
    print(string)

>>> print_text('hello')
hello

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

def make_string_upper(func):
    def inner(string):
        func(string.upper())
    return inner

>>> print_upper_string = make_string_upper(print_text)
>>> print_upper_string('hello')
HELLO

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

def make_string_upper(func):
    def inner(string):
        func(string.upper())
    return inner

@make_string_upper
def print_text(string):
    print(string)

В этом примере мы передаём декорированной функции один аргумент string. Передавать можно любые аргументы, как в обычной фукнции. Попробуем добавить немного функциональности нашему декоратору:

def make_string_upper(func):
    def inner(string):
        print('BEFORE')
        func(string.upper())
        print('AFTER')
    return inner

@make_string_upper
def print_text(string):
    print(string)

>>> print_text('hello')
BEFORE
HELLO
AFTER
>>> print(print_text.__name__))
inner

Так как в декораторе мы возвращаем inner, то и имя функции становится inner. Если имя функции нам важно, то нужно перезаписать его в нашем декораторе:

def make_string_upper(func):
    def inner(string):
        func(string.upper())
    inner.__name__ = func.__name__
    return inner

@make_string_upper
def print_text(string):
    print(string)

>>> print(print_text.__name__)
print_text

Если функцию, которыю мы хотим декорировать, нужно выполнять только при определённых условиях, можно передать это условие в параметрах:

def if_seen(conditional, message):
    def dec(wrapped):
        def inner(*args, **kwargs):
            if not conditional:
                return wrapped(*args, **kwargs)
            else:
                print(message)
        return inner
    return dec

@if_seen(True, 'I have seen you today')
def make_hello():
    print('Oh Hello!')

>>> make_hello()
I have seen you today

Вернуть inner сразу нельзя, потому что получим бесконечную рекурсию. Также декораторы можно комбинировать, пример обёртки html тэгами:

def wrap_div(func):
    def inner(string):
        print('<div>', end='')
        func(string)
        print('</div>')
    return inner

def wrap_p(func):
    def inner(string):
        print('<p>', end='')
        func(string)
        print('</p>', end='')
    return inner

@wrap_div
@wrap_p
def wrapped_text(string):
    print(string, end='')

def another_wrapped_text(string):
    print(string, end='')

>>> wrapped_text('Text wrapped out with html tags')
<div><p>Text wrapped out with html tags</p></div>

>>> without_syntax_sugar = wrap_div(wrap_p(another_wrapped_text))
>>> without_syntax_sugar('decorated too')
<div><p>decorated too</p></div>

Вот так просто можно использовать декораторы в python.