Python: отображение функции по рекурсивным итерам

У меня есть произвольно вложенная итерация:

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

и я хотел бы сопоставить функцию над ней, не меняя структуру. Например, я мог бы преобразовать все числа в строки, чтобы получить

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

Есть ли хороший способ сделать это? Я могу создать собственный метод, чтобы выполнить ручной траверс numbers, но я хотел бы знать, есть ли общий способ отображения над рекурсивными итерами.

Кроме того, в моем примере это нормально, если strings дает мне вложенные списки (или некоторые итерабельные) довольно вложенные кортежи.

Ответ 1

Мы сканируем каждый элемент в последовательности и переходим к более глубокой рекурсии, если текущий элемент является подпоследовательностью или дает его отображение, если мы достигли типа данных без последовательности (может быть int, str, или любые сложные классы).

Мы используем collections.Sequence для обобщения идеи для каждой последовательности, а не только кортежей или списков, и type(item) при выходе, чтобы гарантировать, что подпоследовательности, которые мы получаем, остаются того же типа, что и они.

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)

Демо:

>>> numbers = (1, 2, (3, (4, 5)), 7)
>>> mapped = recursive_map(numbers, str)
>>> tuple(mapped)
('1', '2', ('3', ('4', '5')), '7')

Или более сложный пример:

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

Если вы хотите увеличить свой результат до dict, set и других, вы можете использовать ответ Уриэля:

from collections import Sequence, 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, Sequence):
        return type(data)(apply(v) for v in data)
    else:
        return func(data)

Ввод тестов:

recursive_map({0: [1, {2, 2, 3}]}, str)

Урожайность:

{0: ['1', '{2, 3}']}

Ответ 4

Я расширил понятие рекурсивной карты для работы со стандартными коллекциями Python: 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)

Это проходит следующие тесты, которые я буду включать в основном в качестве примеров использования:

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

Все ранее упоминали о количестве вещей, которые могут понадобиться для любого вида функции flatten, но было кое-что, с чем я играл в качестве упражнения в изучении языка (так что предупреждение Python noob), которое я не видел вполне вместе здесь. По сути, я хотел, чтобы мой flatten мог обрабатывать любые Iterable любой длины и вложенности наиболее эффективным (во времени и пространстве) способом. Это привело меня к шаблону генератора, и первым требованием, которое я поставил для своей функции, было то, что до его создания ничего не было создано.

Моим другим требованием было отсутствие каких-либо явных циклов (for/while), потому что почему бы и нет: по крайней мере, после полезного добавления yield from Python 3.3 я был почти уверен, что это возможно. Конечно, это должно быть рекурсивно, но заставить его получить "плоский" генератор оказалось сложнее, чем я думал. Итак, вот мой 2p, иллюстрирующий замечательную chain и, я подозреваю, тип ситуации (немного более абстрактной, конечно), для которой она была сделана:

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

К сожалению, для моего бесплатного амбициозного проекта (и эго), согласно некоторым довольно грубым тестам, это примерно на 30% медленнее, чем версия, используемая for:

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

вариант которого уже дал Уриэль. Однако я надеюсь, что это хорошая иллюстрация гибкости и мощи Python, используемого квази-функциональным способом, особенно для других новичков в языке.

Редактировать: чтобы and not isinstance(item,(str,bytes)) строки в отдельных элементах списка, можно добавлять, and not isinstance(item,(str,bytes)) к условному условию. И другие различные навороты, которые отвлекают внимание от сути дела.