Python: декоратор обобщенного преобразования ввода

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

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

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

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!'

Но что, если мы хотим преобразовать другие или все входные данные функции? Опять же, написание конкретного декоратора является базовым, но, похоже, не существует единственного естественного способа написать (параметризованную) оболочку для общего случая.

Есть идеи?

Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
154
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Кажется, нельзя избежать наложения протокола на функцию предварительной обработки, если только нет неясного способа изящно справиться с загадкой 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 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!'

preprocess возвращает (преобразованный) (args, kwargs) кортеж

Вы можете получить (вроде) лучшее из обоих миров, если функция предварительной обработки вернет преобразованные аргументы (кортеж) и kwargs (словарь).

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'

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