Декораторы в Python

Вероятно, почти каждый разработчик на Python сталкивался с декораторами, видя конструкцию с со знаком @:

@app.route('/')
def index():
    return "Hello, World!"

Разберемся, что такое декоратор, и как он работает. Этот вопрос часто спрашивают на собеседованиях.

Декоратор – это функция, которая принимает как аргумент другую функцию*. Цель декоратора – расширить функциональность переданной ему функции без непосредственного изменения кода самой функции. Вот и все!

* Примечание: декорировать можно и класс, но об этом расскажу потом!

В Python функция – тоже объект, и ее можно передавать как аргумент, возвращать из другой функции, ей также можно назначать атрибуты.

Символ собачка (@) – всего лишь синтаксический сахар:

@decorator
def foo():
    ...

# эквивалентно:

def foo():
    ...
foo = decorator(foo)

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

def decorator(f):
    return f

А можно вернуть и вообще другую функцию. Например, определенную внутри декоратора (да, внутри функций можно определять другие функции):

def decorator(f):
    def inner():
        print('inner')
    return inner

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

def decorator(f):
    def inner():
        print('begin')
        f()
        print('end')
    return inner


@decorator
def foo():
    print('foo')

foo()
# begin foo
# foo
# end foo

Здесь используется «замыкание», которое сохраняет переменную f в особой области видимости (enclosing), что дает возможность обращаться к f из inner после выхода из функции decorator.

Часто неизвестно, какие аргументы принимает f. Поэтому аргументы обычно обобщают, называя их *args (все позиционные аргументы, как список), **kwargs (все именованные аргументы, как словарь). Эти две штуки охватывают все возможные аргументы. Так же у функции может быть возвращаемое значение, которое неплохо также вернуть из inner. Улучшим наш декоратор, добавив прозрачную передачу любых (заранее неизвестных) аргументов и возвращение результата декорированной функции:

def decorator(f):
    def inner(*args, **kwargs):
        print('begin')
        result = f(*args, **kwargs)
        print('end')
        return result
    return inner


@decorator
def foo(x, y):
    print(f'summing {x} and {y}...')
    return x + y

print(foo(5, y=10))
# begin
# summing 5 and 10...
# end
# 15
@wraps

Помимо самого кода функции и ее аргументов, у нее также есть и другие свойства, например ее настоящее имя или «docstring» (это строчка с описанием функции в начале ее тела, которая хранится в атрибуте __doc__ и выводится при вызове help). Декоратор из прошлого примера потеряет документацию и имя функции (она станет зваться inner):

@decorator
def foo(x, y):
    """Doc string here"""
    return x + y


help(foo)
# Help on function inner in module __main__:
# inner(*args, **kwargs)

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

Шутка про @wraps

Вот теперь название и документация поступят в обертку из оригинальной функции:

from functools import wraps

def decorator(f):
    @wraps(f)
    def inner(*args, **kwargs):
        print('begin')
        result = f(*args, **kwargs)
        print('end')
        return result
    return inner

@decorator
def foo(x, y):
    """Doc string here"""
    return x + y

help(foo)

# Help on function foo in module __main__:

# foo(x, y)
#    Doc string here
Композиция декораторов

Можно применить несколько декораторов к одной функции. Вообще говоря, результат зависит от порядка следования декораторов: тот, что ближе к определению функции воздействует на нее раньше того, что дальше. Пример декораторов для пары HTML тэгов (для простоты я опустил формальности передачи аргументов и атрибутов из предыдущей части). foo и bar декорированы в разном порядке:

def bold(f):
    def inner():
        return '<b>' + f() + '</b>'
    return inner


def italic(f):
    def inner():
        return '<i>' + f() + '</i>'
    return inner


@bold
@italic
def foo():
    return 'foo text'

@italic
@bold
def bar():
    return 'bar text'

print(foo())  # <b><i>foo text</i></b>
print(bar())  # <i><b>bar text</b></i>

И результат разный, потому что:

foo = italic(bold(foo))
bar = bold(italic(bar))

Пока хватит. В следующих частях:

  1. Параметры для декораторов
  2. Декоратор как класс
  3. Декорирование методов класса
  4. Примеры декораторов свои и из стандартных модулей
  5. Декорирование классов

🧙 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Добавить комментарий