Получение плоского представления вложенного списка

Можно ли в Python получить плоское представление списка списков, которое динамически адаптируется к изменениям исходного вложенного списка?

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

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

Простой пример:

a = ["a", "b", "c"]
b = ["d", "e", "f"]
view = flat_view([a, b])
# `view` should show ["a", "b", "c", "d", "e", "f"]
b[0] = "x"
# `view` should show ["a", "b", "c", "x", "e", "f"]

Реализация flat_view() — это то, что я ищу.

Я не знаю ни о какой «родной» способности сделать это. Исторически сложилось так, что вы используете memoryview, чтобы превратить такое представление, доступное только для чтения, в Buffer, но в стандартной библиотеке Python нет готового плоского представления.

Maximilian Burszley 07.08.2024 16:16

Это напоминает мне кусочки Го. В Python, когда вы создаете новый список, он не является указателем/ссылкой на оригинал.

Mr. Polywhirl 07.08.2024 16:20

Это верно для срезов, которые создают копию, @Mr.Polywhirl, но списки являются byref, и изменение переменной, которая указывает на тот же список, что и другой, изменит ее для обоих держателей.

Maximilian Burszley 07.08.2024 16:23

Создание представления в одном списке описано в stackoverflow.com/q/3485475/3001761, также выравнивание кажется весьма специфичным для вашего варианта использования. Неясно, какая функциональность вам понадобится, помимо простого визуального отображения плоской формы (итерация, индексация,...?). Возможно, вы также захотите рассмотреть возможность слабых ссылок на базовые данные.

jonrsharpe 07.08.2024 16:27
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
4
82
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вам нужно будет создать класс, содержащий ссылку на исходные списки.

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

Вы можете получить доступ к любому элементу списка со сложностью поиска O(log n) (двоичный поиск) без использования дополнительной памяти для хранения плоского списка.

Выполнение

Импорт следующих типов:

from typing import Any, Callable, Iterable, List, Tuple, Union

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

Мы хотим быть уверены, что self._notify() вызывается при каждом изменении списка.

class ListWrapper:
    def __init__(self, lst: List[Any]):
        self._list: List[Any] = lst
        self.callbacks: List[Callable[[], None]] = []

    def __getitem__(self, index: int) -> Any:
        return self._list[index]

    def __setitem__(self, index: int, value: Any) -> None:
        self._list[index] = value
        self._notify()

    def __len__(self) -> int:
        return len(self._list)

    def append(self, item: Any) -> None:
        self._list.append(item)
        self._notify()

    def extend(self, iterable: Iterable[Any]) -> None:
        self._list.extend(iterable)
        self._notify()

    def insert(self, index: int, item: Any) -> None:
        self._list.insert(index, item)
        self._notify()

    def remove(self, item: Any) -> None:
        self._list.remove(item)
        self._notify()

    def pop(self, index: int = -1) -> Any:
        item = self._list.pop(index)
        self._notify()
        return item

    def clear(self) -> None:
        self._list.clear()
        self._notify()

    def _notify(self) -> None:
        for callback in self.callbacks:
            callback()

    def add_callback(self, callback: Callable[[], None]) -> None:
        self.callbacks.append(callback)

Вот класс, который обеспечивает «плоское представление» нескольких списков, где обновления исходных списков отражаются в плоском представлении.

class FlatView:
    def __init__(self, lists: List[ListWrapper]) -> None:
        self.lists = lists
        self.update_lengths()
        for lst in self.lists:
            lst.add_callback(self.update_lengths)

    def update_lengths(self) -> None:
        self.sub_lengths = self._compute_overall_length()

    def _compute_overall_length(self) -> List[int]:
        lengths = [0]
        for lst in self.lists:
            lengths.append(lengths[-1] + len(lst))
        return lengths

    def __getitem__(self, index: int) -> Any:
        if index < 0 or index >= len(self):
            raise IndexError("list index out of range")
        list_index = self._find_list_index(index)
        sublist_index = index - self.sub_lengths[list_index]
        return self.lists[list_index][sublist_index]

    def _find_list_index(self, index: int) -> int:
        # Binary search to find the list that contains the index
        low, high = 0, len(self.sub_lengths) - 1
        while low < high:
            mid = (low + high) // 2
            if self.sub_lengths[mid] <= index < self.sub_lengths[mid + 1]:
                return mid
            elif index < self.sub_lengths[mid]:
                high = mid
            else:
                low = mid + 1
        return low

    def __len__(self) -> int:
        return self.sub_lengths[-1]

    def __repr__(self) -> str:
        return repr([item for lst in self.lists for item in lst])

Применение

Следующая функция-обертка создает экземпляр FlatView для предоставленного списка списков.

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

def flat_view(lists: List[List[Any]]) -> Tuple[FlatView, List[ListWrapper]]:
    wrappers = [ListWrapper(lst) for lst in lists]
    return FlatView(wrappers), wrappers

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

Например, b[0] = "x" не будет работать.

if __name__ == "__main__":
    a = ["a", "b", "c"]
    b = ["d", "e", "f"]
    view, [a_wrapper, b_wrapper] = flat_view([a, b])
    
    print(view)                # Output: ['a', 'b', 'c', 'd', 'e', 'f']
    
    b_wrapper[0] = "x"
    print(view)                # Output: ['a', 'b', 'c', 'x', 'e', 'f']
    
    print(view[3], len(view))  # Output: 'x' 6
    
    a_wrapper.append("y")
    print(view, len(view))     # Output: ['a', 'b', 'c', 'y', 'x', 'e', 'f'] 7

    print(a, len(a))           # Output: ['a', 'b', 'c', 'y'] 4
    print(b, len(b))           # Output: ['x', 'e', 'f'] 3

Не тема вопроса, но, по моему мнению, строки документации слишком многословны, что делает код более трудным для чтения. например вы можете просто добавить аннотацию типа, например -> FlatView, и избавиться от Returns: FlatView: An instance of the FlatView class.

mkrieger1 07.08.2024 16:30

Мне нравится этот ответ, поскольку он решает вопрос, но я думаю, что платить O(N) за индексацию - это плохо, и ее можно улучшить.

Maximilian Burszley 07.08.2024 16:36

@MaximilianBurszley Есть компромисс между использованием памяти и производительностью. Я думаю, что O(log n) встречается посередине.

Mr. Polywhirl 07.08.2024 16:52

В вопросе говорится, что «подсписки не должны быть [...] привязаны к статическому размеру, а должны иметь возможность свободно сжиматься или расширяться». Это не предусмотрено при вычислении sub_lengths только один раз при инициализации.

mara004 07.08.2024 17:34

«Если вам нужна сложность поиска O(1), вам нужно будет сохранить копию сглаженного списка». Копирование будет означать, что это больше не представление, поэтому не знаю, почему эта фраза находится в ответе.

mara004 07.08.2024 17:37

@mara004 mara004 ок, я удалил его и убедился, что представление знает об изменениях. Предостережение в том, что вам нужно изменить обертки.

Mr. Polywhirl 07.08.2024 19:54

@Mr.Polywhirl Спасибо!

mara004 08.08.2024 15:39

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

class FlatView:
    def __init__(self, sequence):
        self.sequence = sequence
    def __iter__(self):
        yield from FlatView.flatten(self.sequence)
    @staticmethod
    def flatten(item):
        for i in item:
            if hasattr(i, '__iter__') and not isinstance(i, str):
                yield from FlatView.flatten(i)
            else:
                yield i
    
a = ["a", "b", "c"]
b = ["d", "e", "f"]
c = [a,b]
view = FlatView(c)
print(list(view)) # ['a', 'b', 'c', 'd', 'e', 'f']
b[0] = "x"
print(list(view)) # ['a', 'b', 'c', 'x', 'e', 'f']
c.append(["g", "h", "i"])
print(list(view)) # ['a', 'b', 'c', 'x', 'e', 'f', 'g', 'h', 'i']
print(c)          # [['a', 'b', 'c'], ['x', 'e', 'f'], ['g', 'h', 'i']]

Я думаю, что здесь упущен один момент, а именно возможность индексации самого представления для изменения членов. Вам нужно будет логически рассматривать каждый список как непрерывную часть более крупного списка. Хотя я мог бы экстраполировать это как требование. @OP нужно будет уточнить.

Maximilian Burszley 07.08.2024 16:31

Размышляя о значении представления, вероятно, нет необходимости изменять члены, но я думаю, что ожидаю возможности сделать это view[7] == "g".

Maximilian Burszley 07.08.2024 16:34

@MaximilianBurszley Что, если ваша исходная структура содержит неупорядоченные объекты - «представление не должно ограничиваться примитивным типом, но иметь возможность обрабатывать произвольные объекты». Например, если какой-либо из подконтейнеров представляет собой набор, а не список, индексация в нем бессмысленна.

matszwecja 07.08.2024 16:36

Ах, это разумно. Я думаю, что они имели в виду, это на усмотрение ОП.

Maximilian Burszley 07.08.2024 16:38

@matszwecja @MaximilianBurszley Извините за путаницу. Под «не следует ограничиваться примитивным типом, но иметь возможность обрабатывать произвольные объекты» я имел в виду значения в контейнерах, а не в самих контейнерах, чтобы не вносить какие-то ненужные исправления dtype. Кроме того, я не ищу рекурсивное сглаживание, а просто depth=1 сглаживание.

mara004 07.08.2024 17:49

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

from collections.abc import Sequence

def generate_refs(nested, prefix):
    for i, value in enumerate(nested):
        idx = prefix + [i] 
        if isinstance(value, Sequence) and not isinstance(value, str):
            yield from generate_refs(value, idx)
        else:
            yield idx 

class FlatView:
    def __init__(self, nested):
        self._nested = nested

    def __getitem__(self, n): 
        if not isinstance(n, int):
            raise TypeError("integer indices only")
        if n < 0:
            raise IndexError("negative indices not supported")
        gen = generate_refs(self._nested, [])
        try:
            for _ in range(n+1):
                ref = next(gen)
        except StopIteration:
            raise IndexError("index out of range") from None
        # print(f"debug: position [{n}]: {ref}")
        value = self._nested
        for i in ref:
            value = value[i]
        return value

a = ["a", "b", "c"]
b = ["d", "e", "f"]
test = [a, [b, "g", "h"], "i"]

fw = FlatView(test)
print("nested:", test)
print("flattened:", list(fw))
print("item [5]:", fw[5])
a.extend(["c2", "c3"]) ; print("extended the list 'a'")
print("item [5]:", fw[5])

Выход:

nested: [['a', 'b', 'c'], [['d', 'e', 'f'], 'g', 'h'], 'i']
flattened: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
item [5]: f
extended the list 'a'
item [5]: d

Вопрос требует, чтобы «подсписки не были ограничены статическим размером, но имели возможность свободно расширяться или сжиматься», что не предусмотрено при вычислении ссылок только один раз при инициализации.

mara004 07.08.2024 17:24

@ mara004 mara004 Я обновил свой ответ, чтобы выполнять вычисления во время доступа.

VPfB 07.08.2024 17:52

Другие вопросы по теме