Python: линейный __getitem__ для пары списков списков

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

class MyClass:
    def __init__(self):
        #
        self.instructions = []

        # for demo purposes
        self.instructions.append([0, 1, 2])
        self.instructions.append([3, 4, 5, 6])
        self.instructions.append([7, 8])

    def __getitem__(self, ind):
        if ind >= 0:
            iterator = self.instructions.__iter__()
            compare = int.__gt__
            inc = int.__add__
        else:
            iterator = reversed(self.instructions)
            compare = int.__le__
            inc = int.__sub__

        s = 0
        for tp in iterator:
            L = len(tp)
            if compare(inc(s, L), ind):
                return tp[ind-s]
            else:
                s = inc(s, L)
        else:
            raise IndexError('index out of range')

Это работает. Например

>>> x = MyClass()
>>> x[5]
5
>>> x[-5]
4

Теперь мне нужно изменить класс, чтобы он теперь хранил два списка списков. Это два списка instructions и annotations, и оба имеют одинаковую длину. Но len(instructions[i]) не обязательно должно быть равно len(annotations[i]).

class NewClass:
    def __init__(self):
        #
        self.instructions = []
        self.annotations = []

        # for demo purposes
        self.instructions.append([0, 1, 2])
        self.instructions.append([5, 6, 7, 8])
        self.instructions.append([12, 13])
        
        self.annotations.append([3, 4])
        self.annotations.append([9, 10, 11])
        self.annotations.append([14, 15, 16])

    def __getitem__(self, ind):
        pass

Я хочу сделать это подписным, с порядком элементов, колеблющимся между подсписками instructions и подсписками annotations. Демонстрационные данные указывают порядок подписки. Я хочу

>>> y = NewClass()
>>> y[9]
9
>>> y[-4]
13

Каков эффективный способ сделать это?

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

прекратите использовать такие методы дандера: compare = int.__gt__ используйте модуль operator

juanpa.arrivillaga 10.04.2023 23:01

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

juanpa.arrivillaga 10.04.2023 23:03

Что именно __getitem__ должен вернуть? Вы просто пытаетесь объединить подсписки в одну плоскую итерацию?

chepner 10.04.2023 23:49

Всегда ли instructions и annotations будут иметь одинаковую длину?

Kelly Bundy 11.04.2023 00:12

Какова длина ваших фактических списков (как внешних, так и внутренних списков)?

Kelly Bundy 11.04.2023 00:14

Будете ли вы изменять списки (внутренний и внешний) после создания экземпляра MyClass (в частности, между несколькими вызовами метода __getitem__?

Kelly Bundy 11.04.2023 00:44

Кстати, почему у вас вообще такие разделенные списки?

Kelly Bundy 11.04.2023 00:44

Что вы имеете в виду под словом "линейный" в названии?

Kelly Bundy 11.04.2023 01:17

@KellyBundy Да, instructions и annotations имеют одинаковую длину. Но подсписки - нет. Общее количество предметов может превышать 100 тыс. Списки дополняются пользовательскими операциями и могут изменять длину между различными вызовами getitem.

Abdullah Khalid 11.04.2023 02:22

Полезно знать общее количество элементов, но большая разница, есть ли у вас много коротких списков или только несколько, но длинных списков.

Kelly Bundy 11.04.2023 02:25

@KellyBundy Библиотека представляет собой библиотеку моделирования, в которой пользователи имеют широкую свободу в том, как они заполняют структуры данных, в зависимости от того, что они моделируют. Поэтому я хочу иметь достаточно одинаковую производительность как для многих коротких списков, так и для нескольких длинных списков, чтобы не раздражать пользователя.

Abdullah Khalid 11.04.2023 02:52
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
4
11
141
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

Хотя ваша реализация хороша, я хотел бы поделиться своим собственным способом итерации с использованием chain.from_iterable. Потому что в основном мы цепляем элементы, будь то в начале или в конце.

Для одного списка:

Единственная часть, которая нуждается в объяснении, это map(reversed, reversed(self.instructions)). Нам нужно перевернуть не только весь список, но и отдельные подсписки.

from itertools import chain

class MyClass:
    def __init__(self):
        self.instructions = [
            [0, 1, 2],
            [3, 4, 5, 6],
            [7, 8],
        ]

    def __getitem__(self, ind):
        if ind >= 0:
            chunks = self.instructions
            range_parameter = ind + 1
        else:
            chunks = map(reversed, reversed(self.instructions))
            range_parameter = abs(ind)

        iterator = chain.from_iterable(chunks)

        try:
            for _ in range(range_parameter):
                n = next(iterator)
        except StopIteration:
            raise IndexError("index out of range")

        return n

x = MyClass()
print(x[5])
print(x[-5])

Для двух списков:

Поскольку вы сказали, что нам нужно колебаться, zip — правильный инструмент для этого. Когда ind положительный, все просто. Мы заархивируем их и используем chain.from_iterable два раза, потому что в противном случае это дает нам отдельные подсписки.

Если ind отрицательное, нам нужно два reversed() перед сжатием. Один для внешних списков и один для подсписков.

from itertools import chain

class MyClass:
    def __init__(self):
        self.instructions = [
            [0, 1, 2],
            [5, 6, 7, 8],
            [12, 13],
        ]

        self.annotations = [
            [3, 4],
            [9, 10, 11],
            [14, 15, 16],
        ]

    def __getitem__(self, ind):
        if ind >= 0:
            chunks = zip(self.instructions, self.annotations)
            range_parameter = ind + 1
        else:
            chunks = zip(
                map(reversed, reversed(self.annotations)),
                map(reversed, reversed(self.instructions)),
            )
            range_parameter = abs(ind)

        iterator = chain.from_iterable(chain.from_iterable(chunks))

        try:
            for _ in range(range_parameter):
                n = next(iterator)
        except StopIteration:
            raise IndexError("index out of range")

        return n

x = MyClass()
print(x[9])
print(x[-4])

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

Kelly Bundy 11.04.2023 00:19

Спасибо. Ваш был лучшим среди рабочих ответов. Я принял ваш как правильный.

Abdullah Khalid 12.04.2023 02:10

Вот мой подход:

import itertools


class NewClass:
    def __init__(self):
        #
        self.instructions = []
        self.annotations = []

        # for demo purposes
        self.instructions.append([0, 1, 2])
        self.instructions.append([5, 6, 7, 8])
        self.instructions.append([12, 13])
        
        self.annotations.append([3, 4])
        self.annotations.append([9, 10, 11])
        self.annotations.append([14, 15, 16])
        
    def __iter__(self):
        zipped = itertools.zip_longest(self.instructions, self.annotations, fillvalue=[])
        for sub_lists in zipped:
            yield from itertools.chain.from_iterable(sub_lists)

    def __getitem__(self, key):
        flat = list(self)
        return flat[key]

Примечания

  • Я создал метод __iter__, который позволяет вызывающему объекту перебирать объект следующим образом:

      x = NewClass()
      for e in x:
          print(e)
    
  • __getitem__ построен на __iter__

  • О данных zipped: концептуально вы можете рассматривать это как

      [
          [[0, 1, 2], [3, 4]],          # This is a sub_lists
          [[5, 6, 7, 8], [9, 10, 11]],  # a sub_lists
          ...
      ]
    
  • Выражение itertools.chain.from_iterable(sub_lists) в основном сглаживает sub_lists от [[0, 1, 2], [3, 4]] до [0, 1, 2, 3, 4]

  • Это решение работает для произвольного количества списков, а не только для 2.

Обновлять

Я исправил __getitem__ для обработки отрицательного индекса за счет производительности. Мне лень создавать более эффективное решение.

это не обрабатывает отрицательные показатели

juanpa.arrivillaga 10.04.2023 23:40

Обновлен для обработки отрицательного индекса

Hai Vu 10.04.2023 23:44

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

Davis Herring 11.04.2023 00:27

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

div, mod = divmod(ind, 2)
if mod:
    return get_item(second_list, div)
else:
    return get_item(first_list, div)

Стандартный баланс между стоимостью хранения и стоимостью времени выполнения для произвольного доступа к (несохраняемой) конкатенации нескольких массивов заключается в хранении таблицы смещений начала каждого списка (т. е. суммы длин каждого списка перед ним). ) и используйте бинарный поиск в этой таблице:

import itertools
import bisect

class Index:
    def __init__(self,ll):
        self.ll = ll
        self.oo = list(itertools.accumulate(map(len,ll), initial=0))

    def __getitem__(self, i):
        if i < 0:
            i += self.oo[-1]
        j = bisect.bisect(self.oo,i)
        if not 0 < j <= len(self.ll):
            raise IndexError
        return self.ll[j-1][i-self.oo[j-1]]

    def __iter__(self):
        return itertools.chain.from_iterable(self.ll)

# Example:
i = Index(
  [[9,1,7],
   [3,0],
   [],
   [4,4,4,2]]
)
assert i[4]==0 and i[8]==2

j-1 потому, что начальный 0 приводит к тому, что i из 0 назначается точка вставки 1. Вы можете опустить ,initial=0 (и фактически последний элемент self.oo) за счет более сложного кода для края/ошибки случаи. __iter__ предоставляется, потому что он асимптотически быстрее, чем индексация с последовательными целыми числами, каждое из которых должно подвергаться бинарному поиску, даже если обычно будет найден один и тот же подсписок.

Очевидно, расширение этого для поддержки чередования двух списков (одинаковой длины) тривиально: суммируйте чередующиеся длины, а затем используйте divmod(j-1,2), чтобы получить индекс в списке и выбор между двумя списками (соответственно).

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

juanpa.arrivillaga 11.04.2023 00:43

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

juanpa.arrivillaga 11.04.2023 00:51

Бинарный поиск заставляет меня сказать «о, конечно». Не могли бы вы записать решение из двух списков, поскольку это то, о чем я конкретно прошу? Спасибо.

Abdullah Khalid 11.04.2023 02:36

@AbdullahKhalid Однако вам придется каким-то образом обновить таблицу смещений для ваших изменений в списках. Возможно, вы захотите узнать, как это делает SortedList.

Kelly Bundy 11.04.2023 02:45

@AbdullahKhalid: Если вы меняете подсписки (с двумя списками подсписков или без них), это совсем другая проблема, поэтому мы должны сначала решить ее.

Davis Herring 11.04.2023 07:36

вот моя идея, для версии 2 списка

сначала мы определяем функцию для переключения между списками

from itertools import zip_longest

def alternate(*iterables: "list[list[Any]]") -> "Iterator[list[Any]]":
    for group in zip_longest(*iterables):
        for it in group:
            if it is not None:
                yield it
            

как это может чередоваться между любым количеством ваших списков

теперь для класса

class MyClass:
    def __init__(self):
        self.instructions = [
            [0, 1, 2],
            [5, 6, 7, 8],
            [12, 13],
        ]

        self.annotations = [
            [3, 4],
            [9, 10, 11],
            [14, 15, 16],
        ]
        
    def __len__(self):
        return sum(map(len,alternate(self.instructions, self.annotations)))
    
    def __getitem__(self, index:int ):
        size = len(self)
        if index >= 0:
            if index >= size:
                raise IndexError(index)
        else:
            new_index = size + index
            if 0 <= new_index < size:
                index = new_index
            else:
                raise IndexError(index)
        for item in alternate(self.instructions, self.annotations):
            n = len(item)
            if 0 <= index < n:
                return item[index]
            index -= n
        raise RuntimeError("this should not happens")

test = MyClass()
print(test[9])
print(test[-4])

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

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

и поскольку python такой крутой, вы получаете бесплатно, что ваш класс является итерируемым (просто имея и __getitem__, которые принимают межэлементные значения) и обратимым (также имея __len__)

>>> list(test)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
>>> list(reversed(test))
[16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

в качестве альтернативы вы можете использовать n-й рецепт

from itertools import islice
   
def nth(iterable, n, default=None):
    "Returns the nth item or a default value"
    return next(islice(iterable, n, None), default)   

(или из more_itertools сторонней библиотеки)

а затем вместе с itertools.chain вы можете изменить последний цикл в getitem для return nth(chain.from_iterable(alternate(self.instructions, self.annotations)), index), учитывая, что мы уже проверяем, попадает ли индекс в диапазон

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

Kelly Bundy 11.04.2023 00:55

@KellyBundy конечно, но вы всегда можете сделать свой собственный, если это необходимо, python может сделать для вас не так много, чтобы дать вам бесплатные вещи

Copperfield 11.04.2023 00:58

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