An idea for fast function composition

Boris Borcic bborcic at gmail.com
Sat Feb 16 18:57:26 EST 2008


castironpi at gmail.com wrote:
> 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


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

 >>> f3 = compose(double, square, succ, float)
 >>> import dis
 >>> dis.dis(f3)
   3           0 LOAD_DEREF               0 (funcs)
               3 JUMP_IF_FALSE           33 (to 39)
               6 POP_TOP
               7 LOAD_GLOBAL              0 (compose)
              10 LOAD_DEREF               0 (funcs)
              13 LOAD_CONST               1 (-1)
              16 SLICE+2
              17 CALL_FUNCTION            1
              20 LOAD_DEREF               0 (funcs)
              23 LOAD_CONST               1 (-1)
              26 BINARY_SUBSCR
              27 LOAD_FAST                0 (args)
              30 CALL_FUNCTION_VAR        0
              33 CALL_FUNCTION            1
              36 JUMP_FORWARD            14 (to 53)
         >>   39 POP_TOP
              40 LOAD_DEREF               0 (funcs)
              43 LOAD_CONST               2 (0)
              46 BINARY_SUBSCR
              47 LOAD_FAST                0 (args)
              50 CALL_FUNCTION_VAR        0
         >>   53 RETURN_VALUE


Mmmhhh...




More information about the Python-list mailing list