Декораторы в 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()