Python: обобщенный преобразователь входных преобразований - PullRequest
1 голос
/ 08 апреля 2019

Написание декоратора для преобразования входов функции: Basic.

Написание функции, которая делает декоратор с преобразованием ввода для любого одиночного входного преобразователя: Easy

Вот один из способов:

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 голосов
/ 08 апреля 2019

Вот несколько ответов на мой вопрос.Я намереваюсь выбрать чей-либо ответ в качестве ответа, если сочту его более полным.

Кажется, нельзя избежать наложения протокола на функцию препроцессора, если только нет неясного способа обработки аргументов /головоломка kwargs изящно(Непонятно только потому, что я не знаю об этом.)

Вот несколько вариантов.

препроцесс возвращает (преобразован) args tuple

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 dict

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) кортеж

Вы можете получить (sorta) лучшийиз обоих миров при наличии функции preprocess возвращают преобразованные 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'
...