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))
к условному условию. И другие различные навороты, которые отвлекают внимание от сути дела.