Пояснення (з прикладами та випадками використання)

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

Необхідні попередні знання

Вивчення теми декораторів у Python вимагає певних базових знань. Нижче наведено перелік концепцій, з якими вам варто бути ознайомленими для кращого розуміння цього матеріалу. Також додано посилання на ресурси для оновлення знань, якщо це необхідно.

Основи Python

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

Також важливим є розуміння деяких об’єктно-орієнтованих принципів, таких як гетери, сетери та конструктори. Якщо ви тільки починаєте знайомство з Python, ось корисні матеріали для початку.

Функції як об’єкти першого класу

Окрім базових знань Python, вам також знадобиться розуміння цього важливого концепту. У Python функції, як і майже все інше, є об’єктами, подібно до `int` або `string`. Це означає, що з ними можна виконувати різноманітні операції, а саме:

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

Фактично, єдиною відмінністю функційних об’єктів від інших є наявність магічного методу `__call__()`.

Сподіваємось, що ви маєте достатній рівень знань. Тепер можна переходити до головної теми.

Що таке декоратор Python?

Декоратор Python – це, по суті, функція, яка приймає іншу функцію як аргумент і повертає її модифіковану версію. Якщо функція `foo` є декоратором, вона приймає функцію `bar` і повертає функцію `baz`.

Функція `baz` є модифікацією `bar`, оскільки в її тілі є виклик функції `bar`. Однак, `baz` може виконувати будь-які дії до та після виклику `bar`. Це може звучати складно, тому давайте розглянемо приклад:

# `foo` – декоратор, приймає функцію `bar` як аргумент
def foo(bar):

    # Створюємо `baz` - модифіковану версію `bar`
    # `baz` викличе `bar`, але може виконувати інші дії до та після виклику
    def baz():

        # До виклику `bar` виводимо повідомлення
        print("Щось")

        # Виконуємо `bar`
        bar()

        # Після виклику `bar` виводимо інше повідомлення
        print("Щось інше")

    # `foo` повертає `baz` - модифіковану версію `bar`
    return baz

Як створити декоратор в Python?

Щоб показати, як створюються та застосовуються декоратори, розглянемо простий приклад. Ми створимо декоратор-логер, який буде реєструвати ім’я функції, яку він декорує, щоразу, коли ця функція виконується.

Спершу визначимо функцію-декоратор. Вона буде приймати `func` як аргумент. `func` – функція, яку ми хочемо “прикрасити”.

def create_logger(func):
    # Тіло функції буде тут

Всередині функції-декоратора ми створимо модифіковану функцію, яка буде записувати назву `func` перед її виконанням.

# Всередині `create_logger`
def modified_func():
    print("Виклик: ", func.__name__)
    func()

Далі, функція `create_logger` поверне модифіковану функцію. Отже, повний код `create_logger` виглядає так:

def create_logger(func):
    def modified_func():
        print("Виклик: ", func.__name__)
        func()

    return modified_func

Таким чином, ми створили декоратор. `create_logger` – це простий приклад функції-декоратора. Він приймає `func` і повертає `modified_func`, яка спочатку виводить назву `func`, а потім її виконує.

Як застосовувати декоратори в Python

Щоб скористатися нашим декоратором, застосовуємо синтаксис `@` ось так:

@create_logger
def say_hello():
    print("Привіт, Світе!")

Тепер, якщо ми викличемо `say_hello()`, отримаємо наступний результат:

Виклик: say_hello
Привіт, Світе!

Що ж робить `@create_logger`? Це застосовує декоратор до `say_hello`. Щоб краще зрозуміти, що відбувається, наступний код досягне того ж результату, що і `@create_logger` перед `say_hello`:

def say_hello():
    print("Привіт, Світе!")

say_hello = create_logger(say_hello)

Іншими словами, один із способів застосування декораторів – явно викликати декоратор, передаючи функцію, як показано вище. Інший, більш стислий спосіб – використовувати синтаксис `@`.

У цьому розділі ми розглянули, як створювати декоратори.

Трохи складніші приклади

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

Коли функція приймає аргументи

Якщо функція, яку ви прикрашаєте, приймає аргументи, модифікована функція повинна отримати ці аргументи і передати їх далі, коли вона викликає оригінальну функцію. Звучить заплутано? Поясню на прикладі `foo-bar`.

Пам’ятайте, `foo` – це декоратор, `bar` – функція, яку ми декоруємо, а `baz` – декорована `bar`. В такому випадку, `bar` прийматиме аргументи, які будуть передані в `baz` під час її виклику. Ось приклад коду для кращого розуміння:

def foo(bar):
    def baz(*args, **kwargs):
        # Тут можна виконати певні дії
        ___
        # Викликаємо `bar`, передаючи `args` і `kwargs`
        bar(*args, **kwargs)
        # Тут також можна виконати певні дії
        ___

    return baz

Якщо `*args` і `**kwargs` виглядають незвично, то це лише покажчики на позиційні та ключові аргументи відповідно.

Важливо зазначити, що `baz` має доступ до аргументів, тому вона може перевіряти їх перед викликом `bar`.

Наприклад, якщо у нас є декоратор `secure_string`, який гарантує, що аргумент, переданий декорованій функції, є рядком, то його реалізація виглядатиме так:

def ensure_string(func):
    def decorated_func(text):
        if type(text) is not str:
             raise TypeError('аргумент до ' + func.__name__ + ' повинен бути рядком.')
        else:
             func(text)

    return decorated_func

Ми можемо декорувати функцію `say_hello` наступним чином:

@ensure_string
def say_hello(name):
    print('Привіт', name)

Тепер протестуємо цей код:

say_hello('Іван') # Повинно виконатися без проблем
say_hello(3) # Повинно викинути виняток

Результат має бути таким:

Привіт Іван
Traceback (most recent call last):
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> say hello(3) # should throw an exception
   File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in decorated_func raise TypeError('argument to + func._name_ + must be a string.')
TypeError: аргумент до say hello повинен бути рядком. $0

Як очікувалося, скрипт вивів “Привіт Іван”, оскільки “Іван” є рядком. Виникла помилка під час спроби вивести “Привіт 3”, оскільки “3” не є рядком. Декоратор `secure_string` можна використовувати для перевірки аргументів будь-якої функції, яка очікує рядок.

Декорування класу

Окрім функцій, ми можемо декорувати класи. Коли декоратор застосовується до класу, декорований метод замінює метод конструктора/ініціалізації класу (`__init__`).

Повертаючись до прикладу `foo-bar`, припустимо, що `foo` – наш декоратор, а `Bar` – клас, який ми декоруємо. Тоді `foo` декорує `Bar.__init__`. Це корисно, коли нам потрібно виконати певні дії перед тим, як створити екземпляр об’єкта типу `Bar`.

Це означає, що код:

def foo(func):
    def new_func(*args, **kwargs):
        print('Виконуємо дії перед створенням екземпляра')
        func(*args, **kwargs)

    return new_func

@foo
class Bar:
    def __init__(self):
        print("В ініціалізаторі")

є еквівалентом коду:

def foo(func):
    def new_func(*args, **kwargs):
        print('Виконуємо дії перед створенням екземпляра')
        func(*args, **kwargs)

    return new_func

class Bar:
    def __init__(self):
        print("В ініціалізаторі")


Bar.__init__ = foo(Bar.__init__)

Створення екземпляра класу `Bar` будь-яким із цих двох способів дасть однаковий результат:

Виконуємо дії перед створенням екземпляра
В ініціалізаторі

Приклади декораторів у Python

Хоча ви можете створювати власні декоратори, деякі з них вже вбудовані в Python. Розглянемо кілька типових декораторів:

@staticmethod

Статичний метод використовується в класі, щоб показати, що метод, який він декорує, є статичним. Статичні методи – це методи, які можуть працювати без створення екземпляра класу. У наступному прикладі ми створюємо клас `Dog` зі статичним методом `bark`.

class Dog:
    @staticmethod
    def bark():
        print('Гав, гав!')

Тепер до методу `bark` можна отримати доступ таким чином:

Dog.bark()

Виконання коду дасть:

Гав, гав!

Як згадувалося в розділі “Як застосовувати декоратори”, декоратори можна використовувати двома способами. Синтаксис `@` є більш лаконічним. Інший спосіб – викликати функцію декоратора, передаючи функцію, яку ми хочемо “прикрасити”, як аргумент. Це означає, що наведений вище код досягає того ж результату, що й код нижче:

class Dog:
    def bark():
        print('Гав, гав!')

Dog.bark = staticmethod(Dog.bark)

Ми можемо викликати метод `bark` таким самим чином:

Dog.bark()

І отримаємо той самий результат:

Гав, гав!

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

@classmethod

Цей декоратор використовується, щоб показати, що метод, який він декорує, є методом класу. Методи класу схожі на статичні методи в тому, що їх можна викликати без створення екземпляра класу.

Основна відмінність полягає в тому, що методи класу мають доступ до атрибутів класу, а статичні методи – ні. Python автоматично передає клас як перший аргумент методу класу під час кожного його виклику. Щоб створити метод класу, ми використовуємо декоратор `classmethod`.

class Dog:
    @classmethod
    def what_are_you(cls):
        print("Я " + cls.__name__ + "!")

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

Dog.what_are_you()

Отримаємо результат:

Я Dog!

@property

Декоратор властивостей використовується для позначення методу як властивості. Повертаючись до прикладу із собакою, створимо метод, що повертає ім’я собаки.

class Dog:
    # Створюємо метод конструктора, який приймає ім'я собаки
    def __init__(self, name):

         # Створюємо приватну властивість `name`
         # Подвійне підкреслення робить атрибут приватним
         self.__name = name

    
    @property
    def name(self):
        return self.__name

Тепер ми можемо отримати доступ до імені собаки як до звичайної властивості:

# Створюємо екземпляр класу
foo = Dog('foo')

# Отримуємо доступ до властивості name
print("Ім'я собаки:", foo.name)

І отримаємо:

Ім'я собаки: foo

@property.setter

Декоратор `property.setter` використовується для створення методу встановлення значення для наших властивостей. Щоб скористатися цим декоратором, потрібно вказати назву властивості, для якої створюється сетер. Наприклад, якщо ви створюєте сетер для властивості `foo`, вашим декоратором буде `@foo.setter`. Ось приклад із собакою:

class Dog:
    # Метод конструктора, що приймає ім'я собаки
    def __init__(self, name):

         # Створюємо приватну властивість `name`
         # Подвійне підкреслення робить атрибут приватним
         self.__name = name

    
    @property
    def name(self):
        return self.__name

    # Створюємо сетер для властивості `name`
    @name.setter
    def name(self, new_name):
        self.__name = new_name

Щоб протестувати сетер, можна використати такий код:

# Створюємо нову собаку
foo = Dog('foo')

# Змінюємо ім'я собаки
foo.name="bar"

# Виводимо ім'я собаки на екран
print("Нове ім'я собаки:", foo.name)

Виконання дасть:

Нове ім'я собаки: bar

Важливість декораторів у Python

Тепер, коли ми знаємо, що таке декоратори, і бачили декілька прикладів, розглянемо, чому вони такі важливі в Python. Декоратори важливі з кількох причин. Ось деякі з них:

  • Можливість повторного використання коду: у прикладі з логуванням, ми можемо використовувати `@create_logger` для будь-якої функції. Це дозволяє додавати логування до всіх функцій, не пишучи його вручну для кожної окремо.
  • Модульність коду: знову ж таки, повертаючись до прикладу з логуванням, за допомогою декораторів ми можемо відокремити основну функцію, наприклад `say_hello`, від іншої потрібної нам функції, наприклад, логування.
  • Вдосконалення фреймворків і бібліотек: декоратори широко використовуються у фреймворках і бібліотеках Python для надання додаткових можливостей. Наприклад, у веб-фреймворках, таких як Flask або Django, декоратори застосовуються для визначення маршрутів, обробки автентифікації або застосування проміжного ПЗ до певних представлень.

На завершення

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

Далі ви можете ознайомитися з нашими статтями про кортежі та використання cURL у Python.