Декоратори
Декоратори надають простий синтаксис для виклику функцій вищого порядку [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'