Search code examples
pythonfunctioneval

Construct a python function programmatically from given components


I have a list of python functions, e.g.,

def f1(x, a): return x + a
def f2(x, y, a, b): return x * a + y * b
def f3(c, d, e): return c * d - e
def f4(y, z): return 2 * y - z
...
my_functions = [f1, f2, f3, f4 ...]

and I want to combine all pairs of them in a predefined way, e.g., by taking a product:

my_products = [product(f,g) for f,g in itertools.combinations(my_functions, 2)]

Thus product takes 2 functions, f and g, and produces a function that computes their product: product(f1,f2) should return

def f1_f2(x, a, y, b):
    return f1(x=x, a=a) * f2(x=x, y=y, a=a, b=b)

With Lisp I would have used a simple macro.

With Perl I would have created a string representation of f1_f2 and used eval.

What do I do with Python? Write a file and load it? Use exec? eval? compile?

PS1. The resulting functions will be passed to curve_fit, so all functions in question (especially the return value of product!) must take positional arguments rather than kw dicts.

The argument functions will have intersecting sets of argument names which have to be handled properly.

PS2 If you think this is an XY Problem please suggest other approaches to generating function arguments for curve_fit.


Solution

  • Under the assumption that all of your functions are reasonable simple, i.e. don't have default values and don't have positional-only arguments, this should work:

    from inspect import signature, Signature
    
    def product(f, g):
        f_sig = signature(f)
        g_sig = signature(g)
        combined = f_sig.parameters | g_sig.parameters
        f_g_sig = Signature(combined.values())
    
        def f_g(*args, **kwargs):
            bound = f_g_sig.bind(*args, **kwargs)
            f_res = f(**{name: bound.arguments[name] for name in f_sig.parameters.keys()})
            g_res = g(**{name: bound.arguments[name] for name in g_sig.parameters.keys()})
            return f_res * g_res
    
        f_g.__signature__ = f_g_sig
        return f_g
    

    We construct a Signature object using a combined argument list from both functions, then use that to bind the incoming arguments. Should even work for mixed positional and keyword arguments. For this to correctly work with curve_fit, we also set the (barely documented) __signature__ attribute.