Python: Map a function over recursive iterables

随声附和 提交于 2020-12-01 09:23:08

问题


I have an arbitrarily nested iterable like so:

numbers = (1, 2, (3, (4, 5)), 7)

and I'd like to map a function over it without changing the structure. For example, I might want to convert all the numbers to strings to get

strings = recursive_map(str, numbers)
assert strings == ('1', '2', ('3', ('4', '5')), '7')

Is there a nice way to do this? I can imaging writing my own method to manually traverse numbers, but I'd like to know if there's a general way to map over recursive iterables.

Also, in my example, it's okay if strings gives me nested lists (or some iterable) rather nested tuples.


回答1:


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')



回答2:


def recursive_map(f, it):
    return (recursive_map(f, x) if isinstance(x, tuple) else f(x) for x in it)



回答3:


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}']}



回答4:


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



回答5:


Everyone before has mentioned the number of things one might possibly need for any flavour of a flatten function, but there was something that I've been playing with as an exercise in learning the language (so Python noob alert) that I didn't see quite put together here. Basically I wanted for my flatten to be able to handle any Iterables, of any length and nesting in the most efficient (time and space) way possible. This lead me to the generator pattern, and the first requirement I posed for my function was nothing to be created before its time.

My other requirement was the absence of any explicit looping (for/while) because why not: at least since the helpful addition of yield from in Python 3.3 I was pretty sure it was possible. It would have to be recursive of course, but getting it to give a proper, "flat" generator proved trickier than I thought. So here's my 2p, illustrating the wonderful chain and, I suspect, the kind of situation (a bit more abstracted of course) it was made for:

from itertools import chain
from collections import Iterable

def flatten(items):
    if isinstance(items,Iterable):
        yield from chain(*map(flatten,items))    
    else:
        yield items

items = [0xf, [11, 22, [23, (33,(4, 5))], 66, [], [77]], [8,8], 99, {42}]
print(list(flatten(items)))

Unfortunately for my for-free ambitious project (and ego), according to some pretty rough benchmarking this is ~30% slower than the version using for:

def flatten(items):
    for item in items:
        if isinstance(item,Iterable):
            yield from flatten(item)
        else:
            yield item

a variant of which was already given by Uriel. I hope it is however a good illustration of the flexibility and power of Python used in a quasi-functional way, especially for others new to the language.

Edit: to avoid splitting up strings in individual list items, one can append and not isinstance(item,(str,bytes)) to the conditional. And other various bells and whistles that would detract from the point.



来源:https://stackoverflow.com/questions/42095393/python-map-a-function-over-recursive-iterables

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!