Skip to content

Decorators

Decorator - Декоратор

Що таке декоратори. Навіщо вони потрібні

Summary

Декоратор - структурний шаблон проєктування, який дозволяє модифікувати функцію або клас без зміни їхнього вихідного коду. У Python декоратор - це функція, яка приймає іншу функцію або клас як аргумент і повертає модифіковану версію цієї функції або класу.

З допомогою декоратора ми можемо додати код до або після виконання функції. Синтаксис декораторів базується на використанні символу @, за яким слідує назва декоратора і перед функцією або класом, який потрібно декорувати.

Основна ідея декораторів полягає в тому, що вони додають додаткову функціональність до існуючої функції або класу, не змінюючи їхнього вихідного коду. Це зроблено шляхом обгортання (wrapping) оригінальної функції або класу в нову функцію або клас, яка виконує додаткову логіку.

Основне використання декораторів включає реалізацію логування, мемоізації, перехоплення виключень, аутентифікації та багато іншого. Завдяки декораторам можна використовувати ці функціональні можливості зокрема для багатьох функцій чи класів, спрощуючи процес розробки та підтримки програмного коду.

Декоратор використовує механізм замикання.

Приклад декоратора в Python, який друкує повідомлення перед виконанням функції.

def decorator(func):  
    def wrapper(*args, **kwargs):  
        print("Some logic before original func")  
        return func(*args, **kwargs)  
    return wrapper  

@decorator  
def my_function():  
    print("Original function")

print(my_function.__name__)  # wrapper # NOTE: because we didn't use wraps
print(my_function.__closure__[0])  # <cell at 0x10...: function object at 0x108e25170>
print(my_function.__closure__[0].cell_contents.__name__)  # my_function

Links

Що може бути декоратором. До чого можна застосовувати декоратор

Декоратором може бути будь-який callable об'єкт: функція, лямбда, клас, екземпляр класу. У випадку з останнім визначається метод __call__.

Декоратор можна застосовувати до будь-якого об'єкта, але найчастіше до функцій, методів і класів. Декорування зустрічається настільки часто, що для нього існує окремий оператор @.

def auth_only(view):
    ...

@auth_only
def dashboard(request):
    ...

Якби не існувало оператора декорування, ми записали би код вище так:

def auth_only(view):
    ...

def dashboard(request):
    ...

dashboard = auth_only(dashboard)

Що станеться, якщо декоратор не повертає нічого

Якщо в тілі функції немає оператора return, виклик верне значення None. Проте результат декоратора замінює декорований об'єкт. У випадку якщо декоратор поверне None, і функція, яку ми декоруємо, також стане None. При спробі викликати її після декорування отримаємо помилку "NoneType is not callable".

Чим відрізняється @foobar від @foobar()

Перший випадок - це звичайне декорування функцією foobar.

Другий випадок - декорування функцією, яку поверне виклик foobar. Інакше це називається параметризований декоратор або фабрика декораторів.

Що таке фабрика декораторів

Це функція, яка повертає декоратор. Наприклад, вам потрібен декоратор для перевірки прав. Логіка перевірки однакова, але прав може бути багато. Щоб не копіювати код, можна використати фабрику декораторів.

from functools import wraps

def has_perm(perm):
    def decorator(view):
        @wraps(view)
        def wrapper(request):
            if perm in request.user.permissions:
                return view(request)
            else:
                return HTTPRedirect('/login')
        return wrapper
    return decorator

@has_perm('view_user')
def users(request):
    ...

Навіщо потрібний wraps

wraps - це декоратор зі стандартного модуля functools, який призначає функції-обгортці ті ж самі атрибути __name__, __module__, __doc__, що й у початкової функції, яку декорують. Це потрібно для того, щоб після декорування функція-обгортка в стек-трейсах виглядала як декорована функція.

from functools import wraps
>>> def my_decorator(func):
...     @wraps(func)
...     def wrapper(*args, **kwargs):
...         print('Calling decorated function')
...         return f(*args, **kwargs)
...     return wrapper
...
>>> @my_decorator
... def example():
... """Docstring"""
...     print('Called example function')
...
>>> example()
Calling decorated function
Called example function
>>> example.__name__
'example'
>>> example.__doc__
'Docstring'