简体   繁体   中英

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 lambda s 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 aa 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 ( ie : 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?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM