15

I have an arbitrarily deep set of nested dictionary:

x = {'a': 1, 'b': {'c': 6, 'd': 7, 'g': {'h': 3, 'i': 9}}, 'e': {'f': 3}}

and I'd like to basically apply a function to all the integers in the dictionaries, so like map, I guess, but for nested dictionaries.

So: map_nested_dicts(x, lambda v: v + 7) would be the sort of goal.

I'm stuck as to the best way to perhaps store the layers of keys to then put the modified value back into its correct position.

What would the best way/approach to do this be?

vaultah
  • 44,105
  • 12
  • 114
  • 143
Jean-Luc
  • 3,563
  • 8
  • 40
  • 78
  • A recursive solution might work. Iterate over the items, if a value is an integer, change it, if the value is a dictionary, pass it in a recursive call. – wwii Oct 04 '15 at 15:44

5 Answers5

24

Visit all nested values recursively:

import collections

def map_nested_dicts(ob, func):
    if isinstance(ob, collections.Mapping):
        return {k: map_nested_dicts(v, func) for k, v in ob.iteritems()}
    else:
        return func(ob)

map_nested_dicts(x, lambda v: v + 7)
# Creates a new dict object:
#    {'a': 8, 'b': {'c': 13, 'g': {'h': 10, 'i': 16}, 'd': 14}, 'e': {'f': 10}}

In some cases it's desired to modify the original dict object (to avoid re-creating it):

import collections

def map_nested_dicts_modify(ob, func):
    for k, v in ob.iteritems():
        if isinstance(v, collections.Mapping):
            map_nested_dicts_modify(v, func)
        else:
            ob[k] = func(v)

map_nested_dicts_modify(x, lambda v: v + 7)
# x is now
#    {'a': 8, 'b': {'c': 13, 'g': {'h': 10, 'i': 16}, 'd': 14}, 'e': {'f': 10}}

If you're using Python 3:

  • replace dict.iteritems with dict.items

  • replace import collections with import collections.abc

  • replace collections.Mapping with collections.abc.Mapping

vaultah
  • 44,105
  • 12
  • 114
  • 143
  • I was wondering if the ```items``` method in the first function might cause trouble but it looks like only mappings or mapping derivatives use this method - any thoughts? – wwii Oct 04 '15 at 16:23
  • @wwii This is some sort of duck typing... Yes, it will work for `dict`s, `dict` subclasses, `ChainMap` and more mapping types. – vaultah Oct 04 '15 at 16:26
  • @wwii but you're probably right, I replaced the `try..except` block with `isinstance(ob, collections.Mapping)` check. – vaultah Oct 04 '15 at 16:33
4

Just to expand on vaultah's answer, if one of your elements can be a list, and you'd like to handle those too:

import collections

def map_nested_dicts_modify(ob, func):
for k, v in ob.iteritems():
    if isinstance(v, collections.Mapping):
        map_nested_dicts_modify(v, func)
    elif isinstance(v, list):
        ob[k] = map(func, v)
    else:
        ob[k] = func(v)
ikku100
  • 809
  • 1
  • 7
  • 16
4

If you need it to work for both lists and dicts in arbitrary nesting:

def apply_recursive(func, obj):
    if isinstance(obj, dict):  # if dict, apply to each key
        return {k: apply_recursive(func, v) for k, v in obj.items()}
    elif isinstance(obj, list):  # if list, apply to each element
        return [apply_recursive(func, elem) for elem in obj]
    else:
        return func(obj)
elgehelge
  • 2,014
  • 1
  • 19
  • 24
0

If you want to avoid dependencies and you need to map a mixed dictionaries/iterables collection with any combination of nesting and deepness, you can use the following solution:

def map_nested_coll(func,obj):
    if '__iter__' in dir(obj) and type(obj) not in (str,bytes):
        if type(obj) == dict:
            return {k:map_nested_coll(func,v) for k,v in obj.items()}
        else:
            return tuple(map_nested_coll(func,x) for x in obj)
    else:
        return func(obj)

In order to retain simplicity, non-dict iterables are converted to tuples (you can convert to list instead of tuple if you like, but converting to tuples is slightly faster). Also, although strings and bytes are iterables, usually you want to apply func on the whole string or bytes, so they are filtered out and not treated like iterables.

The advantage of this solution is that it works with any kind of iterable (even generators like zip, range and map) and handles edge cases well (see below):

>>> func = lambda x: x/2
>>> map_nested_coll(func, dict(a=1,b=dict(c=2,d=[3,(41,42),5]),e=[6,7]))
{'a': 0.5, 'b': {'c': 1.0, 'd': (1.5, (20.5, 21.0), 2.5)}, 'e': (3.0, 3.5)}
>>> map_nested_coll(func, [1,dict(a=2,b=3),(4,5)])
(0.5, {'a': 1.0, 'b': 1.5}, (2.0, 2.5))
>>> map_nested_itr(func, map(lambda x: 1+x, range(3)))
(0.5, 1.0, 1.5)
>>> map_nested_coll(func, 9)
4.5
>>> map_nested_coll(func, [])
()
>>> map_nested_itr(func, dict())
{}
mmj
  • 5,514
  • 2
  • 44
  • 51
-1

I have a more general implementation that can accept any number of containers of any type as parameters.

from collections.abc import Iterable
import types
def dict_value_map(fun, *dicts):
    keys = dicts[0].keys()
    for d in dicts[1:]:
        assert d.keys() == keys
    return {k:fun(*(d[k] for d in dicts)) for k in keys}
def collection_map(fun, *collections):
    assert len(collections) > 0
    if isinstance(collections[0], dict):
        return dict_value_map(fun, *collections)
    if isinstance(collections[0], (tuple, list, set)):
        return type(collections[0])(map(fun, *collections))
    else:
        return map(fun, *collections)
iscollection = lambda v:(isinstance(v,Iterable)and(not isinstance(v,str)))

def apply(fun, *collections, at=lambda collections: not iscollection(collections[0])):
    '''
    like standard map, but can apply the fun to inner elements.
    at: a int, a function or sometype. 
    at = 0 means fun(*collections)
    at = somefunction. fun will applied to the elements when somefunction(elements) is True
    at = sometype. fun will applied to the elements when elements are sometype.
    '''
    if isinstance(at, int):
        assert at >= 0
        if at == 0:
            return fun(*collections)
        else:
            return collection_map(lambda *cs:apply(fun, *cs, at=at-1), *collections)
    if isinstance(at, types.FunctionType):
        if at(collections):
            return fun(*collections)
        else:
            return collection_map(lambda *cs:apply(fun, *cs, at=at), *collections)
    else:
        return apply(fun, *collections, at=lambda eles:isinstance(eles[0], at))

examples:

> apply(lambda x:2*x, [(1,2),(3,4)])  
[(2, 4), (6, 8)]

> apply(lambda a,b: a+b, ([1,2],[3,4]), ([5,6],[7,8]))
([6, 8], [10, 12])

> apply(lambda a,b: a+b, ([1,2],[3,4]), ([5,6],[7,8]), at=1)  
([1, 2, 5, 6], [3, 4, 7, 8])

> apply(lambda a,b: a+b, ([1,2],[3,4]), ([5,6],[7,8]), at=0)  
([1, 2], [3, 4], [5, 6], [7, 8])

> apply(lambda a,b:a+b, {'m':[(1,2),[3,{4}]], 'n':5}, {'m':[(6,7),[8,{9}]],'n':10})  
{'m': [(7, 9), [11, {13}]], 'n': 15}

> apply(str.upper, [('a','b'),('c','d')], at=str)  
[('A', 'B'), ('C', 'D')]

and

> apply(lambda v:v+7, {'a': 1, 'b': {'c': 6, 'd': 7, 'g': {'h': 3, 'i': 9}}, 'e': {'f': 3}})
{'a': 8, 'b': {'c': 13, 'd': 14, 'g': {'h': 10, 'i': 16}}, 'e': {'f': 10}}
guoyongzhi
  • 121
  • 1
  • 3