Python:通用输入转换装饰器

时间:2019-04-08 01:51:46

标签: python python-decorators

编写装饰器以转换函数的输入:基本。

编写一个函数,使其成为任何单输入变压器的输入变换装饰器:

这是一种方法:

def input_wrap_decorator(preprocess):
    def decorator(func):
        def func_wrapper(*args, **kwargs):
            return func(preprocess(*args, **kwargs))
        return func_wrapper
    return decorator

考虑以下功能:

def red_riding_hood(adj, noun='eyes'):
    return 'What {adj} {noun} you have!'.format(adj=adj, noun=noun)

使用示例:

assert red_riding_hood('big') == 'What big eyes you have!'
assert red_riding_hood('long', 'ears') == 'What long ears you have!'

我们的input_wrap_decorator使我们可以轻松地随意变换red_riding_hood的第一个参数:

wrapped_func = input_wrap_decorator(lambda x: x.upper())(red_riding_hood)
assert wrapped_func('big') == 'What BIG eyes you have!'

wrapped_func = input_wrap_decorator(lambda x: 'very ' + x)(red_riding_hood)
assert wrapped_func('big') == 'What very big eyes you have!'

但是,如果我们要转换函数的其他或全部输入,该怎么办?同样,编写一个特定的装饰器是基本的,但是似乎没有一种自然的方法可以为一般情况编写一个(参数化的)包装器。

有什么想法吗?

1 个答案:

答案 0 :(得分:0)

以下是我自己问题的一些答案。如果我觉得更完整,我打算选择其他人的答案作为答案。

似乎没有避免在预处理函数上加上协议的方法,除非有一种模糊的方法来优雅地处理args / kwargs难题。 (仅因为我不知道而遮遮掩掩。)

这里有几种选择。

预处理返回(已转换)args元组

def wrap_args_deco(preprocess):
    """Preprocess needs to return the tuple of args (non-keyworded arguments) 
    that should be passed on to the decorated func."""
    def decorator(func):
        def func_wrapper(*args, **kwargs):
            # NOTE: the only difference with input_wrap_decorator is the * before the preprocess
            return func(*preprocess(*args, **kwargs))  
        return func_wrapper
    return decorator

使用示例:

def trans_args(adj, noun):
    '''adj is capitalized and noun is quoted'''
    return adj.upper(), '"{}"'.format(noun) 
wrapped_func = wrap_args_deco(trans_args)(red_riding_hood)
assert wrapped_func('big', 'eyes') == 'What BIG "eyes" you have!'

这里有一些限制。例如,无论是否要转换它们,都需要为所有没有默认值的参数指定转换。

预处理返回(已转换)kwargs字典

def wrap_kwargs_deco(preprocess):
    """Preprocess needs to return the dict of kwargs (keyworded, or named arguments) 
    that should be passed on to the decorated func."""
    def decorator(func):
        def func_wrapper(*args, **kwargs):
            # NOTE: the only difference with input_wrap_decorator is the ** before the preprocess
            return func(**preprocess(*args, **kwargs))  
        return func_wrapper
    return decorator

示例:

def trans_kwargs(adj, noun):
    '''adj is capitalized and noun is quoted'''
    return {'adj': adj.upper(), 'noun': '"{}"'.format(noun)}
wrapped_func = wrap_kwargs_deco(trans_kwargs)(red_riding_hood)
assert wrapped_func('big', 'eyes') == 'What BIG "eyes" you have!'

预处理返回一个(已转换的)(args,kwargs)元组

通过使预处理函数返回转换后的args(元组)和kwargs(dict),您可以(排序)两全其美。

def wrap_args_and_kwargs_deco(preprocess):
    """Preprocess needs to return a the tuple (arg, kwargs) where 
    arg is the list/tuple of (transformed) non-keyworded arguments and
    kwargs is the dict of (transformed) keyworded (a.k.a. "named") arguments
    that should be passed on to the decorated func."""
    def decorator(func):
        def func_wrapper(*args, **kwargs):
            args, kwargs = preprocess(*args, **kwargs)
            return func(*args, **kwargs)
        return func_wrapper
    return decorator

示例:

def trans_kwargs(adj, noun):
    return (adj.upper(),), {'noun': '"{}"'.format(noun)}
wrapped_func = wrap_args_and_kwargs_deco(trans_kwargs)(red_riding_hood)
assert wrapped_func('big', 'eyes') == 'What BIG "eyes" you have!'

为各个(关键字)参数指定转换器

在可以将转换逻辑基于所有参数值的全局视图的意义上,以上建议的选项比以下选项更笼统。但是在大多数情况下,您可能仅需要根据其名称来转换参数。在这种情况下,这是提供更好界面的解决方案。

from functools import wraps

def transform_args(**trans_func_for_arg):
    """
    Make a decorator that transforms function arguments before calling the function.
    Works with plain functions and bounded methods.
    """
    def transform_args_decorator(func):
        if len(trans_func_for_arg) == 0:  # if no transformations were specified...
            return func  # just return the function itself
        else:
            @wraps(func)
            def transform_args_wrapper(*args, **kwargs):
                # get a {argname: argval, ...} dict from *args and **kwargs
                # Note: Didn't really need an if/else here but I am assuming that...
                # Note: ... getcallargs gives us an overhead that can be avoided if there's only keyword args.
                if len(args) > 0:
                    val_of_argname = inspect.signature(func).bind_partial(*args, **kwargs).arguments
                else:
                    val_of_argname = kwargs
                for argname, trans_func in trans_func_for_arg.items():
                    val_of_argname[argname] = trans_func(val_of_argname[argname])
                # apply transform functions to argument values
                return func(**val_of_argname)

            return transform_args_wrapper

    return transform_args_decorator

下面是一个比其他示例具有更多功能覆盖范围的示例:

# Example with a plain function
def f(a, b, c='default c'):
    return "a={a}, b={b}, c={c}".format(a=a, b=b, c=c)
def prepend_root(x):
    return 'ROOT/' + x

def test(f):
    assert f('foo', 'bar', 3) == 'a=foo, b=bar, c=3'
    ff = transform_args()(f)  # no transformation specification, so function is unchanged
    assert ff('foo', 'bar', 3) == 'a=foo, b=bar, c=3'
    ff = transform_args(a=prepend_root)(f)  # prepend root to a
    assert ff('foo', 'bar', 3) == 'a=ROOT/foo, b=bar, c=3'
    ff = transform_args(b=prepend_root)(f)  # prepend root to b
    assert ff('foo', 'bar', 3) == 'a=foo, b=ROOT/bar, c=3'
    ff = transform_args(a=prepend_root, b=prepend_root)(f)  # prepend root to a and b
    assert ff('foo', 'bar', 3) == 'a=ROOT/foo, b=ROOT/bar, c=3'

test(f)

# Example with a bounded method
class A:
    def __init__(self, sep=''):
        self.sep = sep
    def f(self, a, b, c):
        return f"a={a}{self.sep} b={b}{self.sep} c={c}"

a = A(sep=',')
test(a.f)

# Example of decorating the method on the class itself
A.f = transform_args(a=prepend_root, b=prepend_root)(A.f)
a = A(sep=',')
assert a.f('foo', 'bar', 3) == 'a=ROOT/foo, b=ROOT/bar, c=3'