Создание декоратора, который объединяет две функции без указания вызывающей сигнатуры исходной функции

Я хочу создать декоратор, который объединяет две функции и объединяет параметры из их подписей.

Интерфейс, который я хочу:

def f(a, b, c, d, e, f, g, h, i, j, k, l, m, n):
    # I am using many parameters to explain the need of not
    # needing to type the arguments again.
    return a * b * c * d * e * f * g * h * i * j * k * l * m * n

@combines(f)
def g(o, p, *args, **kwargs):
    return (o + p) * f(*args, **kwargs)

Это должно по существу привести к:

def g(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p):
    return (o + p) * (a * b * c * d * e * f * g 
                      * h * i * j * k * l * m * n)

Причина, по которой я этого хочу, заключается в том, что я действительно не знаю аргументов функции f (Я знаю их, но я не хочу вводить их снова, чтобы сделать их общими.)

Я не уверен, если мне нужно позвонить g с *args а также **kwargs, но я думаю, что это будет необходимо.

Вот как далеко я получил:

import functools
import inspect

def combines(old_func):
    old_sig = inspect.signature(old_func)
    old_parameters = old_sig.parameters
    def insert_in_signature(new_func):
        new_parameters = inspect.signature(new_func).parameters
        for new_parameter in new_parameters:
            if new_parameter in old_parameters.keys():
                raise TypeError('`{}` argument already defined'.format(new_parameter))

        @functools.wraps(new_func)
        def wrapper(*args, **kwargs):
            return old_func(*args, **kwargs) * new_func(*args, **kwargs)

        parms = list(old_parameters.values())
        for arg, par in new_parameters.items():
            if par.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
                parms.append(inspect.Parameter(arg, par.kind))

        wrapper.__signature__ = old_sig.replace(parameters=parms)
        return wrapper
    return insert_in_signature

def f(a, b, c, d, e, f, g, h, i, j, k, l, m, n):
    return a * b * c * d * e * f * g * h * i * j * k * l * m * n

@combines(f)
def g(o, p, *args, **kwargs):
    return (o + p) * f(*args, **kwargs)

Это приводит к желаемой подписи вызова g, Но это не работает.

РЕДАКТИРОВАТЬ, потому что сообщение об ошибке спросили

Например: g(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-23-2775f64e1b3e> in <module>()
----> 1 g(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)

<ipython-input-18-3a843320e4e3> in wrapper(*args, **kwargs)
     13         @functools.wraps(new_func)
     14         def wrapper(*args, **kwargs):
---> 15             return old_func(*args, **kwargs) * new_func(*args, **kwargs)
     16 
     17         parms = list(old_parameters.values())

TypeError: f() takes 14 positional arguments but 16 were given

если я последую за сообщением об ошибке и приведу 14 аргументов с g(1,1,1,1,1,1,1,1,1,1,1,1,1,1):

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-24-052802b037a4> in <module>()
----> 1 g(1,1,1,1,1,1,1,1,1,1,1,1,1,1)

<ipython-input-18-3a843320e4e3> in wrapper(*args, **kwargs)
     13         @functools.wraps(new_func)
     14         def wrapper(*args, **kwargs):
---> 15             return old_func(*args, **kwargs) * new_func(*args, **kwargs)
     16 
     17         parms = list(old_parameters.values())

<ipython-input-18-3a843320e4e3> in g(o, p, *args, **kwargs)
     29 @combines(f)
     30 def g(o, p, *args, **kwargs):
---> 31     return (o + p) * f(*args, **kwargs)

TypeError: f() missing 2 required positional arguments: 'm' and 'n'

Ясно, что моя реализация на самом деле не работает.

2 ответа

Решение

Ваша проблема в том, что вы называете f функционировать дважды с разными параметрами, один раз внутри оригинала g только с ожидаемыми параметрами и один раз внутри оболочки со всеми параметрами.

Вы должны выбрать один, мой совет, чтобы удалить его звонок из оригинала g

Я немного изменил ваш код, но по крайней мере моя версия работает на Python 3.5:

  • подпись обернутой функции перечисляет все параметры для f и для g
  • обернутая функция принимает позиционные и ключевые параметры
  • обернутая функция выдает ошибку, когда получает неверное количество параметров

Вот код:

def combine(ext):
    ext_params = inspect.signature(ext).parameters
    def wrapper(inn):
        inn_params = inspect.signature(inn).parameters
        for k in inn_params.keys():
            if k in ext_params.keys():
                raise TypeError('`{}` argument already defined'.format(
                        k))
        all_params = list(ext_params.values()) + \
                 list(inn_params.values())
        # computes the signature for the wrapped function
        sig = inspect.signature(inn).replace(parameters = all_params)
        def wrapped(*args, **kwargs):
            # signature bind magically processes positional and keyword arguments
            act_args = sig.bind(*args, **kwargs).args  
            ext_args = act_args[:len(ext_params.keys())] #  for external function
            inn_args = act_args[len(ext_params.keys()):] #  for inner function
            return ext(*ext_args) * inn(*inn_args)
        w = functools.update_wrapper(wrapped, inn) # configure the wrapper function
        w.__signature__ = sig   # and set its signature
        return w
    return wrapper

Теперь я могу написать:

>>> @combine(f)
def g(o,p):
    return o+p

>>> help(g)
Help on function g in module __main__:

g(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)

>>> g(1,1,1,1,1,1,1,1,1,1,1,1,1,p=1, o=1, n=1)
2

Я думаю, я понимаю, что вы хотите. Для этого, прежде всего, я рекомендую использовать ООП, чтобы сделать вещи понятнее. Я не думаю, что вам нужен весь этот код для обработки параметров или inspect Модуль тоже. Я думаю, вы должны сделать это просто:

Я думаю, что часть проблемы заключается в том, что вы вызываете обе функции с одинаковыми параметрами, и они рассматриваются как слишком много аргументов или слишком мало, и в этом случае вам нужно будет изменить g, Я думаю, что нет никакого способа выяснить, какие параметры идут к какой функции, поэтому вот несколько решений:

Вот одно из решений:

def combines(func):
    class Decorator:
        def __init__(self, f):
            self.outer_func = func
            self.inner_func = f

        def __call__(self, *args, **kwargs):
            res, a, p = self.inner_func(*args, **kwargs)
            return res * self.outer_func(*a, **p)
    return Decorator


def f(a, b, c, d, e, f, g, h, i, j, k, l, m, n):
    return a * b * c * d * e * f * g * h * i * j * k * l * m * n


@combines(f)
def g(o, p, *args, **kwargs):
   return o+p, args, kwargs

print g(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1) # output: 2

Вот еще одно решение, которое использует третью функцию для объединения двух:

def combines(func1, func2):
    class Decorator:
        def __init__(self, f):
            self.outer_func = func1
            self.inner_func = func2
            self.func = f

        def __call__(self, *args, **kwargs):
            res, a, p = self.inner_func(*args, **kwargs)
            return self.func(res, self.outer_func(*a, **p))
    return Decorator


def f(a, b, c, d, e, f, g, h, i, j, k, l, m, n):
    return a * b * c * d * e * f * g * h * i * j * k * l * m * n


def g(o, p, *args, **kwargs):
    return o+p, args, kwargs


@combines(f, g)
def h(a, b):        # a is g's result
    return a * b    # b is f's result

print h(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)     # output: 2

Написать функцию, чтобы выяснить, какие параметры будут идти, к какой функции будет сложно, если не невозможно, но я думаю, что это должно сработать.

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