Написание декоратора для преобразования входных данных функции: 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!'
Но что, если мы хотим преобразовать другие или все входные данные функции? Опять же, написание конкретного декоратора является базовым, но, похоже, не существует единственного естественного способа написать (параметризованную) оболочку для общего случая.
Есть идеи?
Вот несколько ответов на мой собственный вопрос. Я намерен выбрать чей-то ответ в качестве ответа, если я найду его более полным.
Кажется, нельзя избежать наложения протокола на функцию предварительной обработки, если только нет неясного способа изящно справиться с загадкой 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'