Python: Map a function over recursive iterables
I extended the notion of a recursive map to work on the standard python collections: list, dict, set, tuple:
def recursiveMap(something, func):
if isinstance(something, dict):
accumulator = {}
for key, value in something.items():
accumulator[key] = recursiveMap(value, func)
return accumulator
elif isinstance(something, (list, tuple, set)):
accumulator = []
for item in something:
accumulator.append(recursiveMap(item, func))
return type(something)(accumulator)
else:
return func(something)
This passes the following tests, which I'll include mostly as examples of usage:
from hypothesis import given
from hypothesis.strategies import dictionaries, text
from server.utils import recursiveMap
def test_recursiveMap_example_str():
assert recursiveMap({'a': 1}, str) == {'a': '1'}
assert recursiveMap({1: 1}, str) == {1: '1'}
assert recursiveMap({'a': {'a1': 12}, 'b': 2}, str) == {'a': {'a1': '12'}, 'b': '2'}
assert recursiveMap([1, 2, [31, 32], 4], str) == ['1', '2', ['31', '32'], '4']
assert recursiveMap((1, 2, (31, 32), 4), str) == ('1', '2', ('31', '32'), '4')
assert recursiveMap([1, 2, (31, 32), 4], str) == ['1', '2', ('31', '32'), '4']
@given(dictionaries(text(), text()))
def test_recursiveMap_noop(dictionary):
assert recursiveMap(dictionary, lambda x: x) == dictionary
def recursive_map(f, it):
return (recursive_map(f, x) if isinstance(x, tuple) else f(x) for x in it)
If you want to extend your result to dict
, set
and others, you can use Uriel's answer:
from collections import Collection, Mapping
def recursive_map(data, func):
apply = lambda x: recursive_map(x, func)
if isinstance(data, Mapping):
return type(data)({k: apply(v) for k, v in data.items()})
elif isinstance(data, Collection):
return type(data)(apply(v) for v in data)
else:
return func(data)
Tests input:
recursive_map({0: [1, {2, 2, 3}]}, str)
Yields:
{0: ['1', '{2, 3}']}
We scan every element in the sequence, and proceeds into deeper recursion if the current item is a sub-sequence, or yields it's mapping if we reached a non-sequence data type (could be int
, str
, or any complex classes).
We use collections.Sequence
to generalize the idea for every sequence, and not only tuples or lists, and type(item)
upon yield to ensure that the sub-sequences we get back remains of the same type they were.
from collections import Sequence
def recursive_map(seq, func):
for item in seq:
if isinstance(item, Sequence):
yield type(item)(recursive_map(item, func))
else:
yield func(item)
Demo:
>>> numbers = (1, 2, (3, (4, 5)), 7)
>>> mapped = recursive_map(numbers, str)
>>> tuple(mapped)
('1', '2', ('3', ('4', '5')), '7')
Or a more complex example:
>>> complex_list = (1, 2, [3, (complex('4+2j'), 5)], map(str, (range(7, 10))))
>>> tuple(recursive_map(complex_list, lambda x: x.__class__.__name__))
('int', 'int', ['int', ('complex', 'int')], 'map')