Декоратори

Декоратори надають простий синтаксис для виклику функцій вищого порядку [GeirAH].

Важливо

Існує певне непорозуміння у визначеннях.

Decorator is a function returning another function, usually applied as a function transformation using the @wrapper syntax [docf].

However, that’s no quit enough to describe it. The more complete definition is:

Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors [ref].

Перш ніж розбиратися в декораторах, потрібно зрозуміти, як працюють функції.

Об’єкти першого класу

У Python функції є об’єктами першого класу. Усе в Python є об’єктом. Функції також є об’єктами.

Внутрішні функції

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

Приклад вкладених функцій
def heap_sort(origin: List[int]) -> List[int]:
    """Return a sorted collection using the heap sort algorithm"""

    def heapify(_ds: List[int], _size: int, _idx: int) -> List[int]:
        ...

    ...
    for idx in range(size, -1, -1):
        heapify(result, size, idx)
    ...

Порядок визначення внутрішніх функцій не має значення. Визначення функції не виконує тіло функції; воно виконується лише під час виклику функції. Крім того, внутрішні функції не визначаються до виклику батьківської функції. Вони локально масштабуються до свого батька. Спроба викликати функцію heapify поза heap_sort призведе до виключення NameError.

Функції є об’єктами

Це означає, що функції можна передавати і використовувати як аргументи так само, як і будь-які інші об’єкти (наприклад, int, str тощо).

from typing import Callable


def say_hello(name: str) -> str:
    return f"Hello, {name}!"


def be_awesome(name: str) -> str:
    return f"Yo, {name}!"


def greet_serhii(greeting_func: Callable) -> str:
    return greeting_func("Serhii")


if __name__ == "__main__":
    print(f"{greet_serhii(say_hello) = }")
    print(f"{greet_serhii(be_awesome) = }")

Повернення функцій

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

from typing import Callable


def parent(idx: int) -> Callable:
    def first_child():
        return "this is the first child"

    def second_child():
        return "this is the second child"

    return second if not num % 2 else first


first = parent(1)
second = parent(2)

Примітка

parent повертає самі функції, дужок немає.

Після виконання наведеного вище фрагмента коду, first звертається до функції first_child з внутрішньої області видимості parent. Тепер її можна використовувати для виклику цільової функції, на яку вона посилається.

>>> first()
"this is the first child"
>>> second()
"this is the second child"

Прості декоратори

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

def decorator(func: Callable) -> Callable:
    def wrapper():
        print(f"before {func.__name__} call")
        func()
        print(f"after {func.__name__} call")

    return wrapper  # no wrapper call, return reference to wrapper function

def say_hello():
    print("Hello!")

say_hello_decorated = decorator(say_hello)

Запуск функцій:

>>> say_hello()
Hello!
>>> say_hello_decorated()
before say_hello call
Hello!
after say_hello call

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

>>> say_hello = decorator(say_hello)
>>> say_hello()
before say_hello call
Hello!
after say_hello call

Функція say_hello є посиланням на decorator.<locals>.wrapper, яка сама зв’язана з оригінальною функцією say_hello. Для цього існує синтаксичний цукор, який називається pie-синтаксис. Наступний приклад робить те саме, що і перший приклад декоратора:

def decorator(func: Callable) -> Callable:
    def wrapper():
        print(f"before {func.__name__} call")
        func()
        print(f"after {func.__name__} call")

    return wrapper  # no wrapper call, return reference to wrapper function


@decorator
def say_hello():
    print("Hello!")

Важливо

У Python не існує способу роздекорувати об’єкт. Як тільки щось прив’язано до обгортки декоратора - воно прикрашене назавжди.

Прикрашена функція все одно залишається функцією. Отже, її можна прикрасити ще раз, і ще раз, і ще раз, і ще раз…

from typing import Callable


def bread(func: Callable) -> Callable:
    def wrapper():
        print("<--bread-->")
        func()
        print("<--bread-->")

    return wrapper


def vegetables(func: Callable) -> Callable:
    def wrapper():
        print("~~~salad~~~")
        print("***tomato***")
        func()

    return wrapper


def cheese(func: Callable) -> Callable:
    def wrapper():
        func()
        print("---cheese---")

    return wrapper


@bread
@vegetables
@cheese
def sandwich():
    print("_sliced_meat_")
>>> sandwich()
<--bread-->
~~~salad~~~
***tomato***
_sliced_meat_
---cheese---
<--bread-->

Примітка

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

Передача аргументів в обгортку

Досі у прикладах використовувались прості декоратори. Але що, якщо декорована функція отримає якісь аргументи? Це викличе виключення TypeError, яке скаже, що «аргументи пропущено». Це можна виправити, просто передавши аргументи у внутрішню функцію wrapper.

import logging
from typing import Callable

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def log(func: Callable) -> Callable:
    def wrapper(x, y):
        logger.info("%s called", func.__name__)
        return func(x, y)

    return wrapper


@log
def sum_numbers(x, y):
    return x + y

Якщо ви намагаєтеся створити узагальнений декоратор, ви можете передати *args і **kwargs замість точних аргументів:

from typing import Callable


def generic_decorator(func: Callable) -> Callable:
    def wrapper(*args, **kwargs):
        ...  # do something before
        result = func(*args, **kwargs)
        ...  # do something after

        return result

    return wrapper

Передача аргументів до декоратора

Нарешті прийшов час дізнатися, як передавати аргументи декоратору. Ось проста реалізація декоратора defer, який відкладає виконання функції на деякий час:

import time
from typing import Callable


def slow_down(seconds: int = 3) -> Callable:
    def decorator(func: Callable) -> Callable:
        def wrapper(*args, **kwargs):
            started_at = time.perf_counter()
            time.sleep(seconds)
            result = func(*args, **kwargs)
            completed_in = round(time.perf_counter() - started_at, 2)
            print("Completed in %.2f" % completed_in)

            return result

        return wrapper

    return decorator


@slow_down()
def function_a():
    return 42


@slow_down(10)
def function_b():
    return 24
function_a()
Completed in 3.00
function_b()
Completed in 10.00

Декоратори класів

Існує декілька попередньо визначених декораторів для використання разом з класами. Ось вони:

  • classmethod

  • staticmethod

  • property

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

Методи класу

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

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

Загальноприйнятим використанням classmethod є надання альтернативних ініціалізаторів.

Статичні методи

Цей тип методів не приймає ні параметра self, ні параметра cls (але, звичайно, він може приймати довільну кількість інших параметрів).

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

Пояснити використання staticmethod досить складно. Майже завжди замість статичного методу можна створити спеціальну функцію. Але іноді вам потрібно прив’язати до класу деяку логіку, незалежну від самого класу або його екземплярів, і тоді прийнято інкапсулювати її за допомогою staticmethod.

Властивості

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

Деякі приклади

import datetime


class Person:
    """Person class implementation"""

    def __init__(self, first_name: str, last_name: str) -> None:
        """Initialize instance"""

        self.first_name = first_name
        self.last_name = last_name

    @classmethod
    def from_fullname(cls, name: str) -> "Person":
        """Return a person instance"""

        first_name, last_name = name.rsplit(" ", 1)

        return cls(first_name, last_name)

    @staticmethod
    def format_date(date: datetime.date) -> str:
        """Return a formatted date as string"""

        return date.strftime("%d-%m-%Y")

    @property
    def fullname(self) -> str:
        """Return person's fullname"""

        return " ".join([self.first_name, self.last_name])
>>> sh = Person("Serhii", "Horodilov")
>>> vp = Person.from_fullname("Vladyslav Ponomaryov")
>>> sh.fullname
'Serhii Horodilov'
>>> vp.fullname
'Vladyslav Ponomaryov'