Python decorator in class method

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

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

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

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

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

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

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

Читайте также:  Php url xn p1ai

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

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

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 вполне присутствовал раньше. Интерпретатор в данном месте выполняет примерно следующий код:

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

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"=" for k, v in vars(self).items()] return f"()" 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 ") def cls_method(cls, param): print(f"class ") def static_method(param): print(f"static ") 

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

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

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/'.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() sync_perform(dispatcher, effect) if __name__ == '__main__': main()

Источник

Оцените статью