Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the logic behind this particular Python functions composition?

Consider the following Python snippet concerning functions composition:

from functools import reduce
def compose(*funcs):
    # compose a group of functions into a single composite (f(g(h(..(x)..)))
    return reduce(lambda f, g: lambda *args, **kwargs: f(g(*args, **kwargs)), funcs)


### --- usage example:
from math import sin, cos, sqrt
mycompositefunc = compose(sin,cos,sqrt)
mycompositefunc(2)

I have two questions:

  1. Can someone please explain me the compose "operational logic"? (How it works?)
  2. Would it be possible (and how?) to obtain the same thing without using reduce for this?

I already looked here, here and here too, my problem is NOT understanding what lambda means or reduce does (I think I got, for instance, that 2 in the usage example will be somewhat the first element in funcs to be composed). What I find harder to understand is rather the complexity of how the two lambdas got combined/nested and mixed with *args, **kwargs here as reduce first argument ...


EDIT:

First of all, @Martijn and @Borealid, thank you for your effort and answers and for the time you are dedicating to me. (Sorry for the delay, I do this in my spare time and not always have a a lot...)

Ok, coming to facts now...

About 1st point on my question:

Before anything, I realized what I didn't really got (but I hope I did now) about those *args, **kwargs variadic arguments before is that at least **kwargs is not mandatory (I say well, right?) This made me understand, for instance, why mycompositefunc(2) works with that only one (non keyword) passed argument.

I realized, then, that the example would work even replacing those *args, **args in the inner lambda with a simple x. I imagine that's because, in the example, all 3 composed functions (sin, cos, sqrt) expect one (and one only) parameter... and, of course, return a single result... so, more specifically, it works because the first composed function expect just one parameter (the following others will naturally get only one argument here, that's the result of the previous composed functions, so you COULDN'T compose functions that expect more than one argument after the first one... I know it's a bit contort but I think you got what I'm trying to explain...)

Now coming to what remains the real unclear matter for me here:

lambda f, g: lambda *args, **kwargs: f(g(*args, **kwargs))

How does that lambda nesting "magic" works?

With all the great respect you deserve and I bear you, it seems to me like both of you are wrong coming to the conclusion the final result shall be: sqrt(sin(cos(*args, **kw))). It actually can't be, the order of appliance of the sqrt function is clearly reversed: it's not the last to be composed but the first.

I say this because:

>>> mycompositefunc(2)
0.1553124117201235

its result is equal to

>>> sin(cos(sqrt(2)))
0.1553124117201235

whereas you get an error with

>>> sqrt(sin(cos(2)))
[...]
ValueError: math domain error

(that's due to trying to squareroot a negative float)

#P.S. for completeness:

>>> sqrt(cos(sin(2)))
0.7837731062727799

>>> cos(sin(sqrt(2)))
0.5505562169613818

So, I understand that the functions composition will be made from the last one to the first ( i.e. : compose(sin,cos,sqrt) => sin(cos(sqrt(x))) ) but the "why?" and how does that lambda nesting "magic" works? still remains a bit unclear for me... Help/Suggestions very appreciated!

On 2nd point (about rewriting compose without reduce)

@Martijn Pieters: your first compose (the "wrapped" one) works and returns exactly the same result

>>> mp_compfunc = mp_compose(sin,cos,sqrt)
>>> mp_compfunc(2)
0.1553124117201235

The unwrapped version, instead, unfortunately loops until RuntimeError: maximum recursion depth exceeded ...

@Borealid: your foo/bar example will not get more than two functions for composition but I think it was just for explanations not intended for answering to second point, right?

like image 796
danicotra Avatar asked Aug 08 '15 12:08

danicotra


People also ask

What is composition in functions in Python?

Function composition is the way of combining two or more functions in such a way that the output of one function becomes the input of the second function and so on.

What is the main reason for creating functions in Python?

In Python, a function is a group of related statements that performs a specific task. Functions help break our program into smaller and modular chunks. As our program grows larger and larger, functions make it more organized and manageable. Furthermore, it avoids repetition and makes the code reusable.

What is meant by composition of function?

In Maths, the composition of a function is an operation where two functions say f and g generate a new function say h in such a way that h(x) = g(f(x)). It means here function g is applied to the function of x. So, basically, a function is applied to the result of another function.


1 Answers

The *args, **kw syntax in both the lambda signature and call syntax are the best way to pass on arbitrary arguments. They accept any number of positional and keyword arguments and just pass those on to a next call. You could write the result of the outer lambda as:

def _anonymous_function_(*args, **kw):
    result_of_g = g(*args, **kw)
    return f(result_of_g)
return _anonymous_function

The compose function can be rewritten without reduce() like this:

def compose(*funcs):
    wrap = lambda f, g: lambda *args, **kw: f(g(*args, **kw))
    result = funcs[0]
    for func in funcs[1:]:
        result = wrap(result, func)
    return result

This does the exact same thing as the reduce() call; call the lambda for the chain of functions.

So, the first two functions in the sequence are sin and cos, and these are replaced by:

lambda *args, **kw: sin(cos(*args, **kw))

This then is passed to the next call as f, with sqrt g, so you get:

lambda *args, **kw: (lambda *args, **kw: sin(cos(*args, **kw)))(sqrt(*args, **kw)))

which can be simplified to:

lambda *args, **kw: sin(cos(sqrt(*args, **kw)))

because f() is a lambda that passes its arguments to the nested sin(cos()) call.

In the end, then, you have produced a function that calls sqrt(), the result of which is passed to cos(), and the output of that is then passed to sin(). The *args, **kw lets you pass in any number of arguments or keyword arguments, so the compose() function can be applied to anything than is callable, provided that all but the first function takes just one argument, of course.

like image 174
Martijn Pieters Avatar answered Sep 29 '22 01:09

Martijn Pieters