Python получение индекса элемента произвольной глубины вложения

Рейтинг: 3Ответов: 3Опубликовано: 22.06.2023

Подскажите пожалуйста, есть ли варианты получения индекса элемента при неизвестной глубине вложения, либо переменной которая указывала бы на путь к такому элементу?

Решаю задачу, где очень желательно делать изменения "на месте", хотя никто не запрещает подменять первоисточник на результат с изменениями.

Предположим у нас есть список:

lst = [
        'el1',
        ['el2', 15, 'text'],
        ['el3', '2.6', ['el4', ['el5']]],
        'el6'
     ]

Из этого списка мы хотим получить путь до элемента 'el5' для преобразования его, к примеру, в кортеж.

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

Могу ошибаться но при этом мы получим ссылку на тот локальный кусочек содержащий искомый элемент. Но для манипуляций Нам понадобиться ссылаться на источник с указанием полного пути индексами. В противном случае преобразования в первоисточнике не произойдут.

Можно получить путь, например в виде списка индексов по мере продвижения по первоисточнику:

def find_elem(lst: list, elem: any) -> list[int] | None:
    '''получаем путь до искомого элемента в виде списка индексов'''
    for i, el in enumerate(lst):
        if isinstance(el, (list, tuple)):
            returned = find_elem(el, elem)
            if returned != None:    #   проверяем был ли во вложенном списке искомый элемент
                return [i, ] + returned
        elif el == elem:
            return [i, ]
    return None #   элемент в переданной коллекции не нашелся


print(find_elem(lst, 'el5'))   #   вернет путь виде списка [2, 2, 1, 0]

Вопрос в том, как этот список преобразовать в needed_elem = lst[2][2][1][0]

P.S. функцию писал на коленке просто что бы показать суть вопроса.

UPD. прилагаю результат применения алгоритмов с путешествием по первоисточнику и локализацией искомого элемента. В итоге первоисточник остается в первозданном виде...

needed_elem = find_elem(lst, 'text')   №   [1, 2]
needed_addr = lst[1][2]
needed_addr = ['text', 'new text']
print(lst)    #    ['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', ['el5']]], 'el6']

lst[1][2] = ['text', 'new text']
print(lst)    #    ['el1', ['el2', 15, ['text', 'new text']], ['el3', '2.6', ['el4', ['el5']]], 'el6']

Как видно из приведенного примера, к изменениям привел только второй вариант обращения.

Ответы

▲ 2Принят

На вопрос уже даны два правильных ответа. Добавить мне почти нечего кроме возможного дизайна для решения задачи.

Решений два. Первое - простое. Второе с синтаксическим сахаром.

Простое решение

locate(list_, value) - генератор, который возвращает все вхождения значения value в список list_. Просматривается как сам список, так и все его подсписки, рекурсивно.

replace_first(list_, old, new) заменяет первое значение old (если оно есть) на new.

replace_all(list_, old, new) заменяет все значения old на new.

def locate(list_, value):
    for i, v in enumerate(list_):
        if v == value:
            yield list_, i
        if isinstance(v, list):
            yield from locate(v, value)


def replace_first(list_, old, new):
    for lst, i in locate(list_, old):
        lst[i] = new
        break


def replace_all(list_, old, new):
    for lst, i in locate(list_, old):
        lst[i] = new


lst = [
    'el1',
    ['el2', 15, 'text'],
    ['el3', '2.6', ['el4', ['el5']]],
    'el6'
]

print(*locate(lst, 'text'))

print(lst)
replace_all(lst, 'text', ['text', 'new text'])
print(lst)
$ python replace.py
(['el2', 15, 'text'], 2)
['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', ['el5']]], 'el6']
['el1', ['el2', 15, ['text', 'new text']], ['el3', '2.6', ['el4', ['el5']]], 'el6']

Решение с мультииндексом

Класс NestedList оборачивает список и предоставляет синтаксис вида:

nlst = NestedList(lst)  # создание обёртки

print(nlst[[1, 2]])     # обращение ко второму индексу
                        # первого подсписка списка

nlst[[1, 2]] = value    # изменение этого же элемента

nlst.locate(value)      # перебирает все мультииндексы
                        # значения value
class NestedList:
    def __init__(self, list_):
        self._list = list_

    def __getitem__(self, index):
        v = self._list
        for i in index:
            v = v[i]
        return v

    def __setitem__(self, index, value):
        v = self._list
        for i in index[:-1]:
            v = v[i]
        v[index[-1]] = value

    def locate(self, value):

        def locate(list_, stack):
            for i, v in enumerate(list_):
                top = stack, i
                if v == value:
                    index = []
                    node = top
                    while node is not None:
                        index.append(node[1])
                        node = node[0]
                    yield index[::-1]
                if isinstance(v, list):
                    yield from locate(v, top)

        yield from locate(self._list, None)



lst = [
    'el1',
    ['el2', 15, 'text'],
    ['el3', '2.6', ['el4', ['el5']]],
    'el6'
]

nlst = NestedList(lst)

print('list =', lst)
i = next(nlst.locate('text'))
print('i =', i)
print('nlst[i] =', nlst[i])
nlst[i] = ['text', 'new text']
print('nlst[i] =', nlst[i])
print('list =', lst)
$ python replace.py
list = ['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', ['el5']]], 'el6']
i = [1, 2]
nlst[i] = text
nlst[i] = ['text', 'new text']
list = ['el1', ['el2', 15, ['text', 'new text']], ['el3', '2.6', ['el4', ['el5']]], 'el6']
▲ 2

создавайте новый список списков, в который будете помещать все элементы из старого рекурсивно, с заменой искомого элемента на нужный. вот пример для списка списков, с кортежами нужно будет допилить:

lst = [
        'el1',
        ['el2', 15, 'text'],
        ['el3', '2.6', ['el4', ['el5']]],
        'el6'
     ]

def replace_items(l, a, b):
    for i, item in enumerate(l):
        if (type(l[i]) == list):
            l[i] = replace_items(l[i], a=a, b=b)
        else:
            if l[i] == a:
                   l[i] = b
    return l

new = replace_items(lst, a='el5', b=0)
print(new)

['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', [0]]], 'el6']

UPDATE

Если вам просто интересно, как можно добраться до элемента, имея список вложенных индексов, то это реализуется так, например:

from functools import reduce 
from operator import getitem
lst = [
        'el1',
        ['el2', 15, 'text'],
        ['el3', '2.6', ['el4', ['el5']]],
        'el6'
     ]
idx = [2, 2, 1, 0]
res = reduce(getitem, idx, lst)
print(res)

el5

Однако, мне не известны законные способы изменить значение таким образом для элементов списка.

UPDATE 2

Как тут справедливо подсказали, достаточно получить объект-самый вложенный список с искомым элементом и индекс этого элемента, тогда можно будет менять значения:

idx = [2, 2, 1, 0]
inner_lst = reduce(getitem, idx[:-1], lst)
inner_lst[idx[-1]] = "lol"
print(lst)

['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', ['lol']]], 'el6']

▲ 2

Когда вы получаете элемент из списка (в том числе глубоко вложенного), вы действительно получаете ссылку, но ссылку не на место во вложенном списке, а на само значение. По этой ссылке нельзя заменить элемент в списке. Можно только мутировать объект (если он изменяемый), тогда его изменение отобразится в исходной структуре.

Чтобы изменить значение внутри вложенного списка, уже нужен сам список (вложенный список, где лежал элемент) и индекс в нем, т.е., условно, две "координаты". Тогда можно будет мутировать список, заменив значение по найденному индексу:

from typing import Any


def find_elem(lst: list, elem: Any) -> tuple[list, int] | None:
    for i, el in enumerate(lst):
        if el == elem:
            return lst, i
        elif isinstance(el, list):
            result = find_elem(el, elem)
            if result is not None:
                return result

    return None


lst = ['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', ['el5']]], 'el6']

position = find_elem(lst, 'el5')
assert position is not None, "Значение не найдено"

print(lst)
position[0][position[1]] = ['text', 'new text']
print(lst)

# Для лучшей читаемости строку position[0][position[1]] = ... можно переписать так
inner_list, i = position
inner_list[i] = ['text', 'new text']

Вывод:

['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', ['el5']]], 'el6']
['el1', ['el2', 15, 'text'], ['el3', '2.6', ['el4', [['text', 'new text']]]], 'el6']