#python #python-decorators
#python #python-декораторы
Вопрос:
Приведенная ниже функция выполняет (кажется, выполняет) эту работу, но, похоже, там гораздо больше котельной плиты, чем необходимо.
Я уверен, что есть более элегантный способ. Тот, который бы выделил часть этого кода, чтобы он меньше походил на работу с копированием / вставкой / редактированием исправлений, чем сейчас.
Обратите внимание, однако, что элегантность — это еще не все: я бы не хотел, чтобы производительность страдала. Например, я мог бы сократить код пополам, выполнив два декоратора: один для преобразования ввода, а другой для преобразования вывода. Но это было бы менее эффективно, чем текущая версия.
def input_output_decorator(preprocess=None, postprocess=None):
def decorator(func):
if inspect.ismethod(func):
if preprocess is not None:
if postprocess is not None: # both pre and post processes
@wraps(func)
def func_wrapper(self, *args, **kwargs):
return postprocess(func(self, preprocess(*args, **kwargs)))
else: # a preprocess but no postprocess
@wraps(func)
def func_wrapper(self, *args, **kwargs):
return func(self, preprocess(*args, **kwargs))
else: # no preprocess
if postprocess is not None: # no preprocess, but a postprocess
@wraps(func)
def func_wrapper(self, *args, **kwargs):
return postprocess(func(*args, **kwargs))
else: # no pre or post process at all
func_wrapper = func
return func_wrapper
else:
if preprocess is not None:
if postprocess is not None: # both pre and post processes
@wraps(func)
def func_wrapper(*args, **kwargs):
return postprocess(func(preprocess(*args, **kwargs)))
else: # a preprocess but no postprocess
@wraps(func)
def func_wrapper(*args, **kwargs):
return func(preprocess(*args, **kwargs))
else: # no preprocess
if postprocess is not None: # no preprocess, but a postprocess
@wraps(func)
def func_wrapper(*args, **kwargs):
return postprocess(func(*args, **kwargs))
else: # no pre or post process at all
func_wrapper = func
return func_wrapper
return decorator
Некоторые примеры использования:
>>> # Examples with "normal functions"
>>> def f(x=3):
... '''Some doc...'''
... return x 10
>>> ff = input_output_decorator()(f)
>>> print((ff(5.0)))
15.0
>>> ff = input_output_decorator(preprocess=int)(f)
>>> print((ff(5.0)))
15
>>> ff = input_output_decorator(preprocess=int, postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff('5')))
Hello 15!
>>> ff = input_output_decorator(postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff(5.0)))
Hello 15.0!
>>> print((ff.__doc__))
Some doc...
>>>
>>> # examples with methods (bounded, class methods, static methods
>>> class F:
... '''This is not what you'd expect: The doc of the class, not the function'''
... def __init__(self, y=10):
... '''Initialize'''
... self.y = y
... def __call__(self, x=3):
... '''Some doc...'''
... return self.y x
... @staticmethod
... def static_method(x, y):
... return "What {} {} you have".format(x, y)
... @classmethod
... def class_method(cls, x):
... return "{} likes {}".format(cls.__name__, x)
>>>
>>> f = F()
>>> ff = input_output_decorator()(f)
>>> print((ff(5.0)))
15.0
>>> ff = input_output_decorator(preprocess=int)(f)
>>> print((ff(5.0)))
15
>>> ff = input_output_decorator(preprocess=int, postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff('5')))
Hello 15!
>>> ff = input_output_decorator(postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff(5.0)))
Hello 15.0!
>>> print((ff.__doc__))
This is not what you'd expect: The doc of the class, not the function
Моя окончательная реализация, основанная на (принятом) ответе @ micky-loo и вдохновленная ответом @a_guest, является:
def input_output_decorator(preprocess=None, postprocess=None):
def decorator(func):
if preprocess and postprocess:
def func_wrapper(*args, **kwargs):
return postprocess(func(preprocess(*args, **kwargs)))
elif preprocess:
def func_wrapper(*args, **kwargs):
return func(preprocess(*args, **kwargs))
elif postprocess:
def func_wrapper(*args, **kwargs):
return postprocess(func(*args, **kwargs))
else:
return func
return wraps(func)(func_wrapper)
return decorator
Ответ №1:
Использование двух разных декораторов обеспечивает гораздо более компактную реализацию. Это правда, что в случае предварительной постобработки у вас будет еще один вызов функции в цепочке, но это вряд ли повлияет на производительность.
import functools
import inspect
def preprocess(pre):
def decorator(func):
if inspect.ismethod(func):
def wrapper(self, *args, **kwargs):
return func(self, pre(*args, **kwargs))
else:
def wrapper(*args, **kwargs):
return func(pre(*args, **kwargs))
return functools.wraps(func)(wrapper)
return decorator
def postprocess(post):
def decorator(func):
def wrapper(*args, **kwargs):
return post(func(*args, **kwargs))
return functools.wraps(func)(wrapper)
return decorator
Комментарии:
1. Спасибо за ваше предложение. На самом деле, именно так я изначально это реализовал, поскольку, очевидно, это намного понятнее. Но, учитывая, что я нахожусь в случае, когда (многие) оформленные функции будут использоваться интенсивно, в цикле, любое снижение производительности будет учитываться. В тех случаях, когда у нас есть только предварительная обработка или постпроцесс, производительность незначительно снижается по сравнению с опцией, опубликованной выше, но при использовании обоих, это 877 нс против 1040 нс на вызов в моих тестах.
2. Я поддержал ваш ответ, хотя, поскольку это полезная перспектива в общем случае.
Ответ №2:
Вам не нужно выполнять проверку inspect. Вложенность if / else может быть сглажена, чтобы сделать код более читаемым.
from functools import wraps
def input_output_decorator(preprocess=None, postprocess=None):
def decorator(func):
if preprocess and postprocess:
@wraps(func)
def func_wrapper(*args, **kwargs):
return postprocess(func(preprocess(*args, **kwargs)))
elif preprocess:
@wraps(func)
def func_wrapper(*args, **kwargs):
return func(preprocess(*args, **kwargs))
elif postprocess:
@wraps(func)
def func_wrapper(*args, **kwargs):
return postprocess(func(*args, **kwargs))
else:
func_wrapper = func
return func_wrapper
return decorator
Комментарии:
1. Пока не уверен, почему предварительная обработка будет работать без выделения self, но работает для моих тестов и в среднем всего на 0,1% медленнее, поэтому приму это.
2. @thorwhalen Это работает только для функций, а не для методов экземпляра (если методы не потребляют,
*args, **kwargs
и вы не проверяетеlen(args)
или что-то еще). Не разделяяself
средства, например-методы, они будут переданыpreprocess
функции, и эта функция должна как-то с этим справиться. Из моего понимания OP это, однако, не так, отсюда и различие черезinspect.ismethod
. Этот ответ в основном напоминает толькоnot inspect.ismethod(func)
ветвь (с изменениемif / else
структуры), полностью отбрасывая проверку.