An idea for fast function composition

castironpi at gmail.com castironpi at gmail.com
Sat Feb 16 18:06:49 EST 2008


On Feb 16, 3:47 pm, Arnaud Delobelle <arno... at googlemail.com> wrote:
> Hi all,
>
> Recently there was a thread about function composition in Python (and
> this was probably not the first).  The fast way to create a
> (anonymous) composite function
>
>      f1 o f2 o ... o fn
>
> in Python is via
>
>      lambda x: f1(f2(...fn(x)...)),
>
> but according to some this is neither the most compact nor the most
> readable.  Below I define a 'compose' function such that the above can
> be written
>
>      compose(f1, f2, ...., fn),
>
> the resulting function being as fast as the lambda version (or maybe
> faster?).  'getcomposer' is a helper function (which in most cases
> will amount to a dictionary lookup).
>
> ----------------------------------
> def getcomposer(nfunc, _cache={}):
>      "getcomposer(n) -> lambda f1, ..., fn:(lambda x: f1(...fn(x)...))"
>      try:
>          return _cache[nfunc]
>      except KeyError:
>          fnames = ['f%s' % i for i in range(nfunc)]
>          call = ''.join('%s(' % f for f in fnames)
>          args = ','.join(fnames)
>          cstr = 'lambda %s:(lambda x:%sx%s)' % (args, call, ')'*nfunc)
>          composer = _cache[nfunc] = eval(cstr)
>          return composer
>
> def compose(*functions):
>      "compose(f1, ..., fn) -> lambda x: f1(f2(...fn(x)...))"
>      return getcomposer(len(functions))(*functions)
>
> # Test
>
> def double(x): return 2*x
> def square(x): return x**2
> def succ(x): return x+1
>
> f1 = compose(double, square, succ, float)
> f2 = lambda x: double(square(succ(float(x))))
>
> def benchmark(f, n=1000000):
>      from time import time
>      from itertools import imap
>      t0 = time()
>      for _ in imap(f1, xrange(n)): pass
>      t1 = time()
>      return t1-t0
>
> print 'compose', benchmark(f1)
> print 'lambda ', benchmark(f2)
> ----------------------------------
>
> marigold:python arno$ python -i simple_compose.py
> compose 1.84630298615
> lambda  1.86365509033
>  >>> import dis
>  >>> dis.dis(f1)
>    1           0 LOAD_DEREF               0 (f0)
>                3 LOAD_DEREF               3 (f1)
>                6 LOAD_DEREF               1 (f2)
>                9 LOAD_DEREF               2 (f3)
>               12 LOAD_FAST                0 (x)
>               15 CALL_FUNCTION            1
>               18 CALL_FUNCTION            1
>               21 CALL_FUNCTION            1
>               24 CALL_FUNCTION            1
>               27 RETURN_VALUE
>  >>> dis.dis(f2)
>   23           0 LOAD_GLOBAL              0 (double)
>                3 LOAD_GLOBAL              1 (square)
>                6 LOAD_GLOBAL              2 (succ)
>                9 LOAD_GLOBAL              3 (float)
>               12 LOAD_FAST                0 (x)
>               15 CALL_FUNCTION            1
>               18 CALL_FUNCTION            1
>               21 CALL_FUNCTION            1
>               24 CALL_FUNCTION            1
>               27 RETURN_VALUE
>
> f1 and f2 are almost exaclty the same but array lookups (LOAD_DEREFs)
> in f1 replace dictionary lookups (LOAD_GLOBALs) in f2.  A C version of
> 'compose' could easily be written that doesn't require the use of a
> python lambda-function (as created by 'getcomposer').
>
> --
> Arnaud

def compose( funcs ):
   def reccompose( *args ):
      return compose( funcs[:-1] )( funcs[-1]( *args ) ) if funcs else
funcs[0]( *args )
   return reccompose



More information about the Python-list mailing list