Ещё одна статья о декораторах в 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/post/587066/


Интересные статьи

Интересные статьи

В английском языке очень много идиом и фразеологизмов. И смысл многих из них просто невозможно понять, если не знаешь значение фразы. В этом материале мы собрали неб...
Вторая часть нашей серии «Почему ваши приложения Spark медленно работают или выходят из строя» следует за первой частью об управлении памятью и посвящена вопросам, возник...
— Люди не из индустрии вечно не понимают программистов: что они там такое сложное делают, если видно только две кнопки? Что за непонятные слова говорят? Почему так много получают? ...
На протяжении многих лет ученые со всего мира занимаются двумя вещами — изобретают и совершенствуют. И порой неясно, что из этого сложнее. Взять, к примеру, обыкновенные светодиоды, которые к...
UX и UI-дизайнеры всё ещё со скепсисом смотрят в сторону голосовых интерфейсов. Одним кажется, что это маркетинговый хайп, который скоро сойдёт на нет. Другие не пользуются голосовыми ассистентам...