Composing functions in python

前端 未结 12 1164
北荒
北荒 2020-11-27 03:56

I have an array of functions and I\'m trying to produce one function which consists of the composition of the elements in my array. My approach is:

def compo         


        
相关标签:
12条回答
  • 2020-11-27 04:14

    I prefer this one due to readability/simplicity

    from functools import reduce
    
    def compose(*fs):
       apply = lambda arg, f: f(arg)
       composition = lambda x: reduce(apply, [x, *fs])
       return composition
    

    the pipe = compose(a, b, c) will first apply a, then b and then c.

    With regard to maintainability (an debugging) I think actually this one is the easiest to use:

    def compose(*fs):
        def composition(x):
            for f in fs:
                x = f(x)
            return x
        return composition
    
    0 讨论(0)
  • 2020-11-27 04:16

    Suppose you have the following functions:

    def square(x): 
        return x**2
    
    def inc(x): 
        return x+1
    
    def half(x): 
        return x/2
    

    Define a compose function as follows:

    import functools
    
    def compose(*functions):
        return functools.reduce(lambda f, g: lambda x: g(f(x)),
                                functions,
                                lambda x: x)
    

    Usage:

    composed = compose(square, inc, inc, half)
    compose(10)
    >>> 51.0
    

    which executes the functions procedurally in the defined order:

    1. square (= 100)
    2. inc (= 101)
    3. inc (= 102)
    4. half (= 51)

    Adapted from https://mathieularose.com/function-composition-in-python/.

    0 讨论(0)
  • 2020-11-27 04:17

    Recursive implementation

    Here's a fairly elegant recursive implementation, which uses features of Python 3 for clarity:

    def strict_compose(*funcs):
        *funcs, penultimate, last = funcs
        if funcs:
            penultimate = strict_compose(*funcs, penultimate)
        return lambda *args, **kwargs: penultimate(last(*args, **kwargs))
    

    Python 2 compatible version:

    def strict_compose2(*funcs):
        if len(funcs) > 2:
            penultimate = strict_compose2(*funcs[:-1])
        else:
            penultimate = funcs[-2]
        return lambda *args, **kwargs: penultimate(funcs[-1](*args, **kwargs))
    

    This is an earlier version which uses lazy evaluation of the recursion:

    def lazy_recursive_compose(*funcs):
        def inner(*args, _funcs=funcs, **kwargs):
            if len(_funcs) > 1:
                return inner(_funcs[-1](*args, **kwargs), _funcs=_funcs[:-1])
            else:
                return _funcs[0](*args, **kwargs)
        return inner
    

    Both would seem to make a new tuple and dict of arguments each recursive call.

    Comparison of all suggestions:

    Let's test some of these implementations and determine which is most performant, first some single argument functions (Thank you poke):

    def square(x):
        return x ** 2
    
    def increment(x):
        return x + 1
    
    def half(x):
        return x / 2
    

    Here's our implementations, I suspect my iterative version is the second most efficient (manual compose will naturally be fastest), but that may be in part due to it sidestepping the difficulty of passing any number of arguments or keyword arguments between functions - in most cases we'll only see the trivial one argument being passed.

    from functools import reduce
    
    def strict_recursive_compose(*funcs):
        *funcs, penultimate, last = funcs
        if funcs:
            penultimate = strict_recursive_compose(*funcs, penultimate)
        return lambda *args, **kwargs: penultimate(last(*args, **kwargs))
    
    def strict_recursive_compose2(*funcs):
        if len(funcs) > 2:
            penultimate = strict_recursive_compose2(*funcs[:-1])
        else:
            penultimate = funcs[-2]
        return lambda *args, **kwargs: penultimate(funcs[-1](*args, **kwargs))
    
    def lazy_recursive_compose(*funcs):
        def inner(*args, _funcs=funcs, **kwargs):
            if len(_funcs) > 1:
                return inner(_funcs[-1](*args, **kwargs), _funcs=_funcs[:-1])
            else:
                return _funcs[0](*args, **kwargs)
        return inner
    
    def iterative_compose(*functions):
        """my implementation, only accepts one argument."""
        def inner(arg):
            for f in reversed(functions):
                arg = f(arg)
            return arg
        return inner
    
    def _compose2(f, g):
        return lambda *a, **kw: f(g(*a, **kw))
    
    def reduce_compose1(*fs):
        return reduce(_compose2, fs)
    
    def reduce_compose2(*funcs):
        """bug fixed - added reversed()"""
        return lambda x: reduce(lambda acc, f: f(acc), reversed(funcs), x)
    

    And to test these:

    import timeit
    
    def manual_compose(n):
        return square(increment(half(n)))
    
    composes = (strict_recursive_compose, strict_recursive_compose2, 
                lazy_recursive_compose, iterative_compose, 
                reduce_compose1, reduce_compose2)
    
    print('manual compose', min(timeit.repeat(lambda: manual_compose(5))), manual_compose(5))
    for compose in composes:
        fn = compose(square, increment, half)
        result = min(timeit.repeat(lambda: fn(5)))
        print(compose.__name__, result, fn(5))
    

    Results

    And we get the following output (same magnitude and proportion in Python 2 and 3):

    manual compose 0.4963762479601428 12.25
    strict_recursive_compose 0.6564744340721518 12.25
    strict_recursive_compose2 0.7216697579715401 12.25
    lazy_recursive_compose 1.260614730999805 12.25
    iterative_compose 0.614982972969301 12.25
    reduce_compose1 0.6768529079854488 12.25
    reduce_compose2 0.9890829260693863 12.25
    

    And my expectations were confirmed: the fastest is of course, manual function composition followed by the iterative implementation. The lazy recursive version is much slower - likely since a new stack frame is created by each function call and a new tuple of functions is created for each function.

    For a better and perhaps more realistic comparison, if you remove **kwargs and change *args to arg in the functions, the ones that used them will be more performant, and we can better compare apples to apples - here, aside from manual composition, reduce_compose1 wins followed by the strict_recursive_compose:

    manual compose 0.443808660027571 12.25
    strict_recursive_compose 0.5409777010791004 12.25
    strict_recursive_compose2 0.5698030130006373 12.25
    lazy_recursive_compose 1.0381018499610946 12.25
    iterative_compose 0.619289995986037 12.25
    reduce_compose1 0.49532539502251893 12.25
    reduce_compose2 0.9633988010464236 12.25
    

    Functions with just one arg:

    def strict_recursive_compose(*funcs):
        *funcs, penultimate, last = funcs
        if funcs:
            penultimate = strict_recursive_compose(*funcs, penultimate)
        return lambda arg: penultimate(last(arg))
    
    def strict_recursive_compose2(*funcs):
        if len(funcs) > 2:
            penultimate = strict_recursive_compose2(*funcs[:-1])
        else:
            penultimate = funcs[-2]
        return lambda arg: penultimate(funcs[-1](arg))
    
    def lazy_recursive_compose(*funcs):
        def inner(arg, _funcs=funcs):
            if len(_funcs) > 1:
                return inner(_funcs[-1](arg), _funcs=_funcs[:-1])
            else:
                return _funcs[0](arg)
        return inner
    
    def iterative_compose(*functions):
        """my implementation, only accepts one argument."""
        def inner(arg):
            for f in reversed(functions):
                arg = f(arg)
            return arg
        return inner
    
    def _compose2(f, g):
        return lambda arg: f(g(arg))
    
    def reduce_compose1(*fs):
        return reduce(_compose2, fs)
    
    def reduce_compose2(*funcs):
        """bug fixed - added reversed()"""
        return lambda x: reduce(lambda acc, f: f(acc), reversed(funcs), x)
    
    0 讨论(0)
  • 2020-11-27 04:18

    One liner:

    compose = lambda *F: reduce(lambda f, g: lambda x: f(g(x)), F)
    

    Example usage:

    f1 = lambda x: x+3
    f2 = lambda x: x*2
    f3 = lambda x: x-1
    g = compose(f1, f2, f3)
    assert(g(7) == 15)
    
    0 讨论(0)
  • 2020-11-27 04:20

    It doesn't work because all the anonymous functions you create in the loop refer to the same loop variable and therefore share its final value.

    As a quick fix, you can replace the assignment with:

    final = lambda x, f=f, final=final: f(final(x))
    

    Or, you can return the lambda from a function:

    def wrap(accum, f):
        return lambda x: f(accum(x))
    ...
    final = wrap(final, f)
    

    To understand what's going on, try this experiment:

    >>> l = [lambda: n for n in xrange(10)]
    >>> [f() for f in l]
    [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
    

    This result surprises many people, who expect the result to be [0, 1, 2, ...]. However, all the lambdas point to the same n variable, and all refer to its final value, which is 9. In your case, all the versions of final which are supposed to nest end up referring to the same f and, even worse, to the same final.

    The topic of lambdas and for loops in Python has been already covered on SO.

    0 讨论(0)
  • 2020-11-27 04:24

    The most reliable implementation I have found is in the 3rd party library toolz. The compose function from this library also deals with docstring for the composition of functions.

    The source code is freely available. Below is a simple example of usage.

    from toolz import compose
    
    def f(x):
        return x+1
    
    def g(x):
        return x*2
    
    def h(x):
        return x+3
    
    res = compose(f, g, h)(5)  # 17
    
    0 讨论(0)
提交回复
热议问题