Search code examples
pythonpython-decorators

Python decorator to transform input and/or output of a function or method


The function below does (seem to do) the job, but it seems there's much more boiler plate than necessary.

I'm sure there's a more elegant way out there. One that would factor some of this code out so that it looks less like the copy/paste/edit patch work that it is now.

Note though, that elegance isn't everything: I wouldn't want performance to suffer. For example, I could cut the code in half by doing two decorators: One for input transformation, and another for output transformation. But this would be less efficient than the current version.

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

Some examples of use:

    >>> # 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

My final implementation, based on @micky-loo's (accepted) answer, and inspired by @a_guest's answer is:


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

Solution

  • You don't need to do the inspect check. The if/else nesting can be flattened to make the code more readable.

    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