Подтвердить что ты не робот

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

Проблема

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

Кажется, что удаление одного элемента из списка Python стоит O (N), поскольку Python должен копировать все элементы над элементом под рукой на одном месте. Кроме того, поскольку количество элементов для удаления приблизительно пропорционально количеству элементов в списке, это приводит к алгоритму O (N ^ 2).

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

Сохранение локального индекса:

while processingdata:
    index = 0
    while index < len(somelist):
        item = somelist[index]
        dosomestuff(item)
        if somecondition(item):
            del somelist[index]
        else:
            index += 1

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

Прокрутка списка назад:

while processingdata:
    for i in xrange(len(somelist) - 1, -1, -1):
        dosomestuff(item)
        if somecondition(somelist, i):
            somelist.pop(i)

Это позволяет избежать увеличения индексной переменной, но в конечном итоге имеет ту же стоимость, что и исходная версия. Он также нарушает логику dosomestuff (item), который хочет обработать их в том же порядке, что и в исходном списке.

Создание нового списка:

while processingdata:
    for i, item in enumerate(somelist):
        dosomestuff(item)
    newlist = []
    for item in somelist:
        if somecondition(item):
            newlist.append(item)
    somelist = newlist
    gc.collect()

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

Использование списков:

while processingdata:
    for i, item in enumerate(somelist):
        dosomestuff(item)
    somelist[:] = [x for x in somelist if somecondition(x)]

Это очень элегантный, но под-обложкой он еще раз перебирает весь список и должен копировать большинство элементов в нем. Моя интуиция заключается в том, что эта операция, вероятно, стоит больше, чем исходная инструкция del, по крайней мере, в памяти. Имейте в виду, что somelist может быть огромным и что любое решение, которое будет проходить через него только один раз за запуск, вероятно, всегда победит.

Использование функции фильтра:

while processingdata:
    for i, item in enumerate(somelist):
        dosomestuff(item)
    somelist = filter(lambda x: not subtle_condition(x), somelist)

Это также создает новый список, занимающий много ОЗУ.

Использование функции фильтрации itertools:

from itertools import ifilterfalse
while processingdata:
     for item in itertools.ifilterfalse(somecondtion, somelist):
         dosomestuff(item)

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

Перемещение элементов вверх по списку при ходьбе

while processingdata:
    index = 0
    for item in somelist:
        dosomestuff(item)
        if not somecondition(item):
            somelist[index] = item
            index += 1
    del somelist[index:]

Это тонкий метод, который кажется экономически эффективным. Я думаю, что он переместит каждый элемент (или указатель на каждый элемент?) Ровно один раз, что приведет к алгоритму O (N). Наконец, я надеюсь, что Python будет достаточно интеллектуальным, чтобы изменить размер списка в конце, не выделяя память для новой копии списка. Не уверен, хотя.

Отказаться от списков Python:

class Doubly_Linked_List:
    def __init__(self):
        self.first = None
        self.last = None
        self.n = 0
    def __len__(self):
        return self.n
    def __iter__(self):
        return DLLIter(self)
    def iterator(self):
        return self.__iter__()
    def append(self, x):
        x = DLLElement(x)
        x.next = None
        if self.last is None:
            x.prev = None
            self.last = x
            self.first = x
            self.n = 1
        else:
            x.prev = self.last
            x.prev.next = x
            self.last = x
            self.n += 1

class DLLElement:
    def __init__(self, x):
    self.next = None
    self.data = x
    self.prev = None

class DLLIter:
    etc...

Этот тип объекта похож на список python ограниченным образом. Однако исключение элемента гарантировано O (1). Мне бы не хотелось идти сюда, так как это потребовало бы большого количества рефакторинга кода почти везде.

4b9b3361

Ответ 1

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

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

def process_and_generate_data(source_iterable):
    for item in source_iterable:
        dosomestuff(item)
        if not somecondition(item):
            yield item

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

Ответ 2

Из вашего описания это звучит как deque ( "колода" ), именно то, что вы ищете:

http://docs.python.org/library/collections.html#deque-objects

"Итерации" через него, повторяя вызов pop(), а затем, если вы хотите сохранить всплывающий элемент в deque, вернув этот элемент вперед с помощью appendleft (item). Чтобы не отставать, когда вы закончите итерацию и все увидели в deque, либо поставьте объект маркера, например None, который вы наблюдаете, либо просто попросите deque len(), когда вы начинаете определенный цикл и используете диапазон ( ) для pop() точно, что много элементов.

Я считаю, что вы найдете все необходимые вам операции, тогда O (1).

Ответ 3

Python хранит только ссылки на объекты в списке - не сами элементы. Если вы вырастите элемент списка по элементу, список (то есть список ссылок на объекты) будет расти один за другим, в конечном итоге дойдя до конца избыточной памяти, которую Python предопределил в конце списка (ссылок!), Затем он копирует список (ссылок!) В новое более крупное место, в то время как элементы списка остаются в своем старом расположении. Так как ваш код все равно посещает все элементы старого списка, копирование ссылок на новый список с помощью new_list [i] = old_list [i] практически не будет носить никакого бремени. Единственный намек на производительность состоит в том, чтобы выделить все новые элементы одновременно, а не добавлять их (OTOH, документы Python говорят, что амортизированное добавление по-прежнему равно O (1), поскольку количество лишних элементов растет с размером списка). Если вам не хватает места для нового списка (ссылок), то я боюсь, что вам не повезло - любая структура данных, которая уклонилась от установки/удаления на месте O (n), вероятно, будет больше, чем простой массив из 4 - или 8-байтовые записи.

Ответ 4

Двунаправленный список хуже, чем просто перераспределение списка. Список Python использует 5 слов + одно слово для каждого элемента. Дважды связанный список будет использовать 5 слов на элемент. Даже если вы используете одноуровневый список, он все равно будет 4 слова на элемент - намного хуже, чем менее 2 слов на элемент, который будет перестраивать список.

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

Для создания нового списка предложенный вами подход не так хорош. Нет никакой очевидной причины, почему вы не могли просто перечислить список один раз. Кроме того, вызов gc.collect() не нужен и на самом деле вреден - подсчет ссылок CPython в любом случае освободит старый список, и даже другие сборщики мусора лучше собирать, когда они нажимают на давление памяти. Так что-то вроде этого будет работать:

while processingdata:
    retained = []
    for item in somelist:
        dosomething(item)
        if not somecondition(item):
            retained.append(item)
    somelist = retained

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

def process_and_decide(item):
    dosomething(item)
    return not somecondition(item)

while processingdata:
    somelist = [item for item in somelist if process_and_decide(item)]

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

def inplace_filter(func, list_):
    pos = 0
    for item in list_:
        if func(item):
            list_[pos] = item
            pos += 1
    del list_[pos:]

while processingdata:
    inplace_filter(process_and_decide, somelist)

Ответ 5

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

Если у вас проблемы с использованием памяти, вы можете отказаться от использования в Python-конструкциях в памяти и перейти с базы данных на диске. Доступно множество баз данных и sqlite поставляется с Python. В зависимости от вашего использования и того, насколько жесткие требования к вашей памяти, array.array или numpy могут вам помочь, но это сильно зависит от того, что вам нужно делать. array.array будет иметь все те же временные сложности, что и list, и массивы numpy, но будут работать по-разному. Использование ленивых итераторов (таких как генераторы и материал в модуле itertools) часто может сократить использование памяти в n раз.

Использование базы данных улучшит время для удаления элементов из произвольных мест (хотя порядок будет потерян, если это важно). Использование dict будет делать то же самое, но потенциально при использовании высокой памяти.

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

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

По-моему, вам придется больше рассказать о своем проблемном классе для более конкретного ответа, но общий совет

  • Переходите по списку, создавая новый список по ходу (или используя генератор для получения элементов, когда они вам понадобятся). Если вам действительно нужен список, это будет иметь коэффициент памяти 2, который масштабируется нормально, но не помогает, если вам не хватает периода памяти.
  • Если у вас нехватка памяти, а не микрооптимизация, вам, вероятно, нужна база данных на диске или для хранения ваших данных в файле.

Ответ 6

Брэндон Крейг Роудс предлагает использовать collections.deque, который может решить эту проблему: для операции не требуется дополнительной памяти, и она сохраняется O (n). Я не знаю общего использования памяти и того, как она сравнивается со списком; стоит отметить, что в deque нужно хранить намного больше ссылок, и я не удивлюсь, если это не так интенсивно, как использование двух списков. Вам нужно будет испытать или изучить его, чтобы знать себя.

Если бы вы использовали deque, я бы развернул его несколько иначе, чем предлагает Родос:

from collections import deque
d = deque(range(30))
n = deque()

print d

while True:
    try:
        item = d.popleft()
    except IndexError:
        break

    if item % 3 != 0:
        n.append(item)

print n

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

Ответ 7

A set (или даже dict) может быть тем, что вы ищете. Это та же самая базовая структура, что и словарь (без связанных значений), но ваши объекты должны быть хешируемыми.

Если порядок важен в вашем списке/наборе, вы можете сделать упорядоченный набор. Существует хороший рецепт для активации OrderedSet. В этот ответ есть еще одно небольшое предложение. Python 2.7 и 3.1 также имеют OrderedDict. Вы должны проверить реализацию для себя, чтобы увидеть, как накладные расходы воздействуют на вас, но выигрыш в скорости с хэш-таблицы вполне может стоить того.

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