Osunderdog
Osunderdog

Reputation: 19

in python is there a better way to apply a list of functions to a dictionary

Is there a better way to iteratively apply a list of functions to a dictionary? Here is an example of what I want to do. But this uses recursion.

def func1(h: dict):
    h['foo']=10
    return(h)

def func2(h: dict):
    h['bar']=100
    return(h)

def func3(h: dict):
    h['baz']=h['foo']+h['bar']
    return(h)

func3(func2(func1({'firstElement':'good'})))

Produces expected output:

{'bar': 100, 'baz': 110, 'firstElement': 'good', 'foo': 10}

I want to provide the functions as a array and produce the same output. Here is what I have tried and works:

def recApply(flist, h=None):
    """
    Helper Apply the list of functions iteratively over the dictionary passed
    :obj: function list each will be applied to the dictionary sequentially.
    """
    #if no dictionary passed, then set the dictionary.
    if(h == None):
        h = {}

    #iteratively call functions with dictionary as a passed parameter and returning a derived dictionary
    for f in flist:
        h = f(h)

    return(h)

flist = [func1,func2,func3]
recApply(flist,{'firstElement':'good'})

This produces the desired output:

{'bar': 100, 'baz': 110, 'firstElement': 'good', 'foo': 10}

Is there a way to do this that is more readable, removes the recApply function and hopefully minimizes dictionary copies?

Upvotes: 0

Views: 105

Answers (3)

tobias_k
tobias_k

Reputation: 82929

You could also use reduce (or functools.reduce in Python 3) with the starting dictionary as the initializer parameter:

>>> from functools import reduce  # Python 3
>>> reduce(lambda (x, f): f(x), (func1, func2, func3), {'firstElement':'good'})
{'bar': 100, 'baz': 110, 'firstElement': 'good', 'foo': 10}

This will apply the functions one after the other to the initializer or the result of the previous function.

You could also combine this with functools.partial to create a chained function that can then be applied to different input dictionaries:

>>> from functools import partial
>>> chain = partial(reduce, lambda (x, f): f(x), (func1, func2, func3))
>>> chain({'firstElement':'good'})
{'bar': 100, 'baz': 110, 'firstElement': 'good', 'foo': 10}

And you could generalize that further, making it a partial of a partial function...

>>> chainer = partial(partial, reduce, lambda (x, f): f(x))
>>> chain = chainer((func1, func2, func3))
>>> chain({'firstElement':'good'})
{'bar': 100, 'baz': 110, 'firstElement': 'good', 'foo': 10}

Upvotes: 1

chepner
chepner

Reputation: 531708

reduce (or functools.reduce in Python 3) can be used to compose a list of functions into a single function. This requires you to define a composition function:

def compose(f, g):
    def _(x):
        return f(g(x))
    return _

and an identity function:

def identity(x):
    return x

Using these, you can create a function g that applies each function in order to an initial input.

g = reduce(compose, [func3, func2, func1], identity)
assert (g({'firstElement': 'good'}) ==
        {'firstElement': 'good', 'foo': 10, 'bar': 100, 'baz': 110})

Note this works because func1, func2 and func3 are similar enough to pure functions that you can make use of the function monoid. Loosely speaking, it just means that function composition is associative (compose(f, compose(g, h)) is the same as compose(compose(f, g), h)) and that the identity function is neutral under composition (compose(identity, f) and compose(f, identity) are both the same as f itself).

Your three functions aren't really pure functions; they are more like identity functions with side effects. However, you can treat them as pure functions because you are using them as if they were defined as, for example,

def func1(h):
    h1 = {}
    h1.update(h)
    h1['foo'] = 10
    return h1

Exercise for the reader: determine if my call to reduce actually defines g(x) = func3(func2(func1(x))) or g(x) = func1(func2(func3(x)).

Upvotes: 2

zwer
zwer

Reputation: 25799

You don't need to return your dictionaries and re-assign references - mutable types are passed by reference so:

def func1(h):
    h['foo'] = 10

def func2(h):
    h['bar'] = 100

def func3(h):
    h['baz'] = h['foo'] + h['bar']

start_dict = {'firstElement': 'good'}

for f in (func1, func2, func3):
    f(start_dict)

print(start_dict)
# {'firstElement': 'good', 'baz': 110, 'bar': 100, 'foo': 10}

would to perfectly fine.

Upvotes: 2

Related Questions