Декораторы методов класса python

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

Источник

Глубокое погружение в декораторы Python

Gigi Sayfan

Gigi Sayfan Last updated Apr 4, 2016

Обзор

Декораторы Python — одна из моих любимых функций Python. Они являются наиболее удобной для пользователя *и* для разработчиков являются отличной реализацией аспектно-ориентированного программирования, которое я не увидел в любом другом языке программирования.

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

Прежде чем перейти к некоторым классным примерам, если вы хотите изучить происхождение декораторов немного больше, тогда функции декораторов появились сначала в Python 2.4. См. PEP-0318 для интересного обсуждения истории, обоснования и выбора имени «декоратор». Декораторы класса появились сначала в Python 3.0. см. PEP-3129, который довольно короткий и построен поверх всех концепций и идей декораторов функций.

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

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

Классическими примерами являются встроенные декораторы @staticmethod и @classmethod. Эти декораторы превращают метод класса в соответствие со статическим методом (не предоставляется первый аргумент self) или метод класса (первым аргументом является класс, а не экземпляр).

Источник

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