Декоратор 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
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
0
2 001
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Использование двух разных декораторов позволяет реализовать гораздо более компактную реализацию. Это правда, что в случае пре-постобработки у вас будет еще один вызов функции в цепочке, но это почти не повлияет на производительность.

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

Спасибо за ваше предложение. Собственно, так я и реализовал изначально, так как, очевидно, это намного понятнее. Но, учитывая, что я нахожусь в случае, когда (многие) оформленные функции будут интенсивно использоваться, в цикле будет учитываться любое снижение производительности. По сравнению с опцией, опубликованной выше, производительность снижается незначительно в случаях, когда у нас есть только предварительная или постобработка, но при использовании обоих это составляет 877 нс против 1040 нс на вызов в моих тестах.

thorwhalen 18.04.2019 21:56

Однако я проголосовал за ваш ответ, так как в общем случае это полезная точка зрения.

thorwhalen 19.04.2019 03:06
Ответ принят как подходящий

Вам не нужно делать контрольную проверку. Вложенность 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

Пока не уверен, почему предварительный процесс будет работать без выделения себя, но работает для моих тестов и в среднем всего на 0,1% медленнее, так что приму это.

thorwhalen 18.04.2019 22:11

@thorwhalen Это работает только для функций, а не для методов-экземпляров (если только методы не используют *args, **kwargs, а вы не проверяете len(args) или что-то в этом роде). Отсутствие выделения self означает, что, например, методы экземпляра будут переданы в функцию preprocess, и эта функция должна как-то с этим справиться. Однако, насколько я понимаю ОП, это не так, отсюда и различие через inspect.ismethod. Этот ответ в основном только напоминает ветку not inspect.ismethod(func) (с изменением структуры if / else), полностью отбрасывая проверку.

a_guest 19.04.2019 12:02

Другие вопросы по теме