Ещё одна статья о декораторах в python, или немного о том, как они работают и как они могут поменять синтаксис языка

от автора

Декораторы в python являются одной из самых часто используемых возможностей языка. Множество библиотек и, особенно, веб-фреймворков предоставляют свой функционал в виде декораторов. У неопытного python разработчика уйдёт не так уж много времени, чтобы разобраться, как написать свой декоратор, благо существует огромное количество учебников и примеров, а опытный разработчик уже не раз писал свои декораторы, казалось бы, что ещё можно добавить и написать о них?

Я постараюсь раскрыть информацию о том, как работают стандартные декораторы staticmethod, classmethod, а так же сам интерпретатор python, как писать декораторы, принимающие аргументы без дважды вложенных функций, ну, и наконец, как немного поменять синтаксис python.

Определение статический метод или нет по сигнатуре, а не по декоратору
Определение статический метод или нет по сигнатуре, а не по декоратору

Базовое определение и простые примеры

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

Декоратор — механизм, позволяющий изменить объект или функции, дополнив или полностью изменив, его работу. Например, добавить логирование, замеры производительности, проверку прав, метрики, обработку ошибок, прикрепить какую-то информацию к объекту или функции.

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

from typing import Optional  from fastapi import FastAPI  app = FastAPI()   @app.get("/") def read_root():     return {"Hello": "World"}   @app.get("/items/{item_id}") def read_item(item_id: int, q: Optional[str] = None):     return {"item_id": item_id, "q": q}

app.get в примере выше регистрирует функции и связывает их с определённым путём, при этом никак не меняя их реализацию.

Однако, можно изменить поведение функции, например, добавить игнорирование исключений

import logging from functools import wraps from typing import Callable   def suppress(f: Callable):     @wraps(f)     def inner(*args, **kwargs):         try:             return f(*args, **kwargs)         except Exception as e:             logging.exception("Something went wrong")      return inner   def f1():     1 / 0   @suppress def f2(x):     x / 0   f2(2)  # -> первое исключение будет залогированно и программа продолжит работать f1()   # -> а вот здесь программа завершится с ошибкой print("I will never be printed") 

@suppress — синтаксис через @ — по сути синтаксический сахар, он появился только в python 2.4 в далёком 2003 году, что, однако, не мешало декораторам существовать в языке. Даже classmethod вполне присутствовал раньше. Интерпретатор в данном месте выполняет примерно следующий код:

f2 = suppress(f2)

То есть это просто вызов функции, которой передаётся другая функция. Осознание этого процесса позволяет понять, как задать декоратор с параметрами. Например, мы хотим игнорировать не все исключения, а лишь некоторые.

Следующий вариант:

def suppress(f: Callable, ex=Exception):    ...   @suppress(ZeroDivisionError) def f2(x):     x / 0

Не сработает, потому что интерпретатор вызовет suppress с ZeroDivisionError в качестве первого аргумента, никакой дополнительной магии здесь не происходит, python просто вызовет функцию и не подумает, что её вызывают в качестве декоратора и, возможно, стоило бы не сразу вызывать её, а создать декорируемую функцию и, например, передать её в качестве первого аргумента, а все остальные, ZeroDivisionError в данном случае — в качестве второго и последующих. Поэтому при первом вызове декоратора там надо создать функцию, которая потом примет декорируемую ф-цию, изменит её работу и вернёт обёртку.

def suppress(ex=Exception):     def dec(f):         @wraps(f)         def inner(*args, **kwargs):             try:                 return f(*args, **kwargs)             except ex as e:                 logging.exception("Something went wrong")         return inner      return dec

Выглядит немного монструозно, но именно так и было принято писать декораторы с параметрами, пока кому-то в голову не пришла светлая идея, что декоратор можно создавать в виде класса.

Реализация декоратора с параметрами в виде класса

По сути нам необходимо разделить этапы создания с запоминанием переданных параметров и вызова, то есть сделать то, что делают классы, поэтому можно изначально использовать их, а не делать всё самому с двумя вложенными функциями.

class suppress:     def __init__(self, ex=Exception):         self._ex = ex          def __call__(self, f: Callable):         @wraps(f)         def inner(*args, **kwargs):             try:                 return f(*args, **kwargs)             except self._ex:                 logging.exception("Something went wrong")         return inner

Мне кажется этот код гораздо лучше читается, осталось теперь только смириться с именем класса, начинающимся с маленькой буквы или с именем декоратора, начинающимся с большой.

Декорирование классов

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

from typing import Type   def auto_str(c: Type):     def str(self):         variables = [f"{k}={v}" for k, v in vars(self).items()]         return f"{c.__name__}({', '.join(variables)})"     c.__str__ = str      return c   class Sample1:     def __init__(self, a, b):         self.a = a         self.b = b   @auto_str class Sample2(Sample1):     def __init__(self, a, b, c):         super().__init__(a, b)         self.c = c   print(str(Sample2(1, 2, 3)))  # -> Sample2(a=1, b=2, c=3)

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

Semantic Self

Меня всегда немного удивляло, что python, заставляя указывать self параметр в сигнатуре каждого метода, никак не использует это своё требование и не делает метод автоматически статическим, если аргументов нет, и не возвращает classmethod, если первый параметр называется cls. Но с помощью декоратора можно исправить данный «недостаток».

import inspect from typing import Type, Callable   def semantic_self(cls: Type):     for name, kind, cls, obj in inspect.classify_class_attrs(cls):         # с помощью модуля inspect возможно пройтись по всем         # атрибутам класса и определить метод ли это         if kind == "method" and not _is_special_name(name):             setattr(cls, name, _get_method_wrapper(obj))     return cls   def _is_special_name(name: str) -> bool:     # специальные методы трогать не будем     return name.startswith("__") and name.endswith("__")   def _get_method_wrapper(obj: Callable):     # определяем есть ли у метода аргументы, и, в зависимости от имени     # первого аргумента, меняем его     args = inspect.getargs(obj.__code__).args     if args:         if args[0] == "self":             return obj         elif args[0] == "cls":             return classmethod(obj)     return staticmethod(obj)

Пример использования:

@semantic_self class Sample:     def obj_method(self, param):         print(f"object {self} {param}")      def cls_method(cls, param):         print(f"class {cls} {param}")      def static_method(param):         print(f"static {param}") 

Реализация декораторов из стандартной библиотеки

Рассмотрим как реализованы некоторые из частоиспользуемых декораторов стандартной бибилиотеки.

abstractmethod реализован весьма прямолинейно: добавлением специального аттрибута __isabstractmethod__. Класс таскает с собой множество абстрактных методов и обновляет их при создании потомков.

    abstracts = set()     # Check the existing abstract methods of the parents, keep only the ones     # that are not implemented.     for scls in cls.__bases__:         for name in getattr(scls, '__abstractmethods__', ()):             value = getattr(cls, name, None)             if getattr(value, "__isabstractmethod__", False):                 abstracts.add(name)     # Also add any other newly added abstract methods.     for name, value in cls.__dict__.items():         if getattr(value, "__isabstractmethod__", False):             abstracts.add(name)     cls.__abstractmethods__ = frozenset(abstracts)     return cls

Ещё интереснее реализован staticmethod, потому что по сути он не делает ничего. Статический метод — это функция определённая в некотором пространстве имён, этот декоратор возвращает саму функцию. А вот обычные методы, не помеченные таким декоратором преобразуются в boundmethod, это можно видеть на КДПВ.

Например, вот так выглядит получение статического метода:

static PyObject * sm_descr_get(PyObject *self, PyObject *obj, PyObject *type) {     staticmethod *sm = (staticmethod *)self;      if (sm->sm_callable == NULL) {         PyErr_SetString(PyExc_RuntimeError,                         "uninitialized staticmethod object");         return NULL;     }     Py_INCREF(sm->sm_callable);     return sm->sm_callable;  // ф-ция возвращается без изменений }

А вот так, обычного:

static PyObject * instancemethod_descr_get(PyObject *descr, PyObject *obj, PyObject *type) {     PyObject *func = PyInstanceMethod_GET_FUNCTION(descr);     if (obj == NULL) {         Py_INCREF(func);         return func;     }     else         return PyMethod_New(func, obj);  // метод ассоциируется с объектом }

В случае класс методов, всё тоже довольно предсказуемо:

static PyObject * cm_descr_get(PyObject *self, PyObject *obj, PyObject *type) {     classmethod *cm = (classmethod *)self;      if (cm->cm_callable == NULL) {         PyErr_SetString(PyExc_RuntimeError,                         "uninitialized classmethod object");         return NULL;     }     if (type == NULL)         type = (PyObject *)(Py_TYPE(obj));     if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {         return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,                                                       type);     }     // метод ассоциируется с типом объекта     return PyMethod_New(cm->cm_callable, type); }

Примеры декораторов

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

@contract(a='int,>0', b='list[N],N>0', returns='list[N]') def my_function(a, b):     ...

Есть библиотеки, реализующие некоторые элементы функционального программирования: например отделение чистого кода от side эффектов. Преобразование функции, генерирующей исключения, в функцию, возвращающую тип Option/Maybe:

@safe def _make_request(user_id: int) -> requests.Response:     # TODO: we are not yet done with this example, read more about `IO`:     response = requests.get('/api/users/{0}'.format(user_id))     response.raise_for_status()     return response

Или алгоритм от способа его выполнения, позволяет выбирать, хотите ли вы выполнять его синхронно или асинхронно:

from effect import sync_perform, sync_performer, Effect, TypeDispatcher  class ReadLine(object):     def __init__(self, prompt):         self.prompt = prompt  def get_user_name():     return Effect(ReadLine("Enter a candy> "))  @sync_performer def perform_read_line(dispatcher, readline):     return raw_input(readline.prompt)  def main():     effect = get_user_name()     effect = effect.on(         success=lambda result: print("I like {} too!".format(result)),         error=lambda e: print("sorry, there was an error. {}".format(e)))      dispatcher = TypeDispatcher({ReadLine: perform_read_line})     sync_perform(dispatcher, effect)  if __name__ == '__main__':     main()


ссылка на оригинал статьи https://habr.com/ru/articles/587066/