Python: yield-and-delete

Как я могу получить объект из генератора и немедленно его забыть, чтобы он не занимал память?

Например, в следующей функции:

def grouper(iterable, chunksize):
    """
    Return elements from the iterable in `chunksize`-ed lists. The last returned
    element may be smaller (if length of collection is not divisible by `chunksize`).

    >>> print list(grouper(xrange(10), 3))
    [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
    """
    i = iter(iterable)
    while True:
        chunk = list(itertools.islice(i, int(chunksize)))
        if not chunk:
            break
        yield chunk

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


EDIT: используя стандартный Python 2.5/2.6/2.7 на python.org.


Решение (предлагаемое почти одновременно @phihag и @Owen): оберните результат небольшим изменчивым объектом и анонимно верните кусок, оставив только небольшой контейнер:

def chunker(iterable, chunksize):
    """
    Return elements from the iterable in `chunksize`-ed lists. The last returned
    chunk may be smaller (if length of collection is not divisible by `chunksize`).

    >>> print list(chunker(xrange(10), 3))
    [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
    """
    i = iter(iterable)
    while True:
        wrapped_chunk = [list(itertools.islice(i, int(chunksize)))]
        if not wrapped_chunk[0]:
            break
        yield wrapped_chunk.pop()

С этой оптимизацией памяти вы можете теперь сделать что-то вроде:

 for big_chunk in chunker(some_generator, chunksize=10000):
     ... process big_chunk
     del big_chunk # big_chunk ready to be garbage-collected :-)
     ... do more stuff

Ответы

Ответ 1

Если вы действительно хотите получить эту функциональность, я полагаю, вы могли бы использовать оболочку:

class Wrap:

    def __init__(self, val):
        self.val = val

    def unlink(self):
        val = self.val
        self.val = None
        return val

И может использоваться как

def grouper(iterable, chunksize):
    i = iter(iterable)
    while True:
        chunk = Wrap(list(itertools.islice(i, int(chunksize))))
        if not chunk.val:
            break
        yield chunk.unlink()

Это по существу то же, что и phihag с pop();)

Ответ 2

После yield chunk значение переменной никогда не будет использоваться снова в функции, поэтому хороший сборщик интерпретатора/мусора уже освободит chunk для сбора мусора (примечание: cpython 2.7, похоже, не делает этого, pypy 1.6 по умолчанию gc делает). Поэтому вам не нужно ничего менять, кроме примера вашего кода, в котором отсутствует второй аргумент grouper.

Обратите внимание, что сборка мусора является недетерминированной в Python. Нулевой сборщик мусора, который вообще не собирает свободные объекты, является абсолютно корректным сборщиком мусора. Из Руководство для Python:

Объекты никогда не уничтожаются явно; однако, когда они становятся они недоступны, они могут быть собраны в мусор. Реализация позволило отложить сборку мусора или вообще опустить ее - это вопрос качества реализации, как сбор мусора при условии, что все объекты не будут собраны достижимы.

Следовательно, не может быть определено, работает ли программа Python или "не занимает память" без указания реализации Python и сборщика мусора. Учитывая конкретную реализацию Python и сборщик мусора, вы можете использовать gc модуль test, освобожден ли объект.

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

def grouper(iterable, chunksize):
    i = iter(iterable)
    while True:
        tmpr = [list(itertools.islice(i, int(chunksize)))]
        if not tmpr[0]:
            break
        yield tmpr.pop()

Вместо списка вы также можете использовать любую другую структуру данных, которая с функцией, которая удаляет и возвращает объект, например Owen wrapper.

Ответ 3

@Radim,

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

Теперь я думаю, что я понял, и я хочу подтвердить.

Я буду представлять ваш код таким образом

import itertools

def grouper(iterable, chunksize):
    i = iter(iterable)
    while True:
        chunk = list(itertools.islice(i, int(chunksize)))
        if not chunk:
            break
        yield chunk

............
............
gigi = grouper(an_iterable,4)
# before A
# A = grouper(an_iterable,4)
# corrected:
A = gigi.next()
# after A
................
...........
# deducing an object x from A ; x doesn't consumes a lot of memory
............
# deleting A because it consumes a lot of memory:
del A
# code carries on, taking time to executes
................
................
......
..........
# before B
# B = grouper(an_iterable,4)
# corrected:
B = gigi.next()
# after B
.....................
........

Ваша проблема в том, что даже в течение времени, прошедшего между # после удаления A, код продолжается, принимая время для выполнения
и
# до B,
объект удаляемого имени "A" все еще существует и потребляет много памяти, потому что между этим объектом и идентификатором "кусок" внутри функции генератора сохраняется привязка?

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

.

@phihag

Вы написали в комментарии:

1)
После yield chunk невозможно получить доступ к значению хранится в куске из этой функции. Поэтому эта функция не держите любые ссылки на объект, о котором идет речь

(Кстати, я бы не написал поэтому, но "потому что" )

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

Давайте подведем порядок.

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

import itertools

def grouper(iterable, chunksize):
    i = iter(iterable)
    chunk = ''
    last = ''
    while True:
        print 'new turn   ',id(chunk)
        if chunk:
            last = chunk[-1]
        chunk = list(itertools.islice(i, int(chunksize)))
        print 'new chunk  ',id(chunk),'  len of chunk :',len(chunk)
        if not chunk:
            break
        yield '%s  -  %s' % (last,' , '.join(chunk))
        print 'end of turn',id(chunk),'\n'


for x in grouper(['1','2','3','4','5','6','7','8','9','10','11'],'4'):
    print repr(x)

результат

new turn    10699768
new chunk   18747064   len of chunk : 4
'  -  1 , 2 , 3 , 4'
end of turn 18747064 

new turn    18747064
new chunk   18777312   len of chunk : 4
'4  -  5 , 6 , 7 , 8'
end of turn 18777312 

new turn    18777312
new chunk   18776952   len of chunk : 3
'8  -  9 , 10 , 11'
end of turn 18776952 

new turn    18776952
new chunk   18777512   len of chunk : 0

.

Однако вы также написали (это начало вашего ответа):

2)
После yield chunk значение переменной никогда больше не используется в функция, поэтому хороший интерпретатор/сборщик мусора уже бесплатный кусок для сбора мусора (примечание: cpython 2.7 похоже не делает это, pypy 1.6 с по умолчанию gc делает).

На этот раз вы не говорите, что функция не содержит больше ссылки chunk после yield chunk, вы говорите, что ее значение не используется снова до возобновления chunk в следующем повороте цикла while. Правильно, в коде Radim объект chunk не используется снова, прежде чем идентификатор "chunk" будет переназначен в команде chunk = list(itertools.islice(i, int(chunksize))) в следующем повороте цикла.

.

Это утверждение № 2 в этой цитате, отличное от предыдущего № 1, имеет два логических последствия:

FIRST, мой вышеприведенный код не может претендовать на то, чтобы строго доказать, что кто-то думает так же, как вы, что есть действительно способ получить доступ к значению куска после инструкции yield chunk.
Поскольку условия в моем предыдущем коде не совпадают, при которых вы утверждаете обратное, то есть: в коде Radim, о котором вы говорите, объект chunk действительно не используется снова до следующего поворота.
И тогда можно притвориться, что это из-за использования chunk в моем предыдущем коде (инструкции print 'end of turn',id(chunk),'\n', print 'new turn ',id(chunk) и last = chunk[-1] действительно используют его), что бывает, что ссылка на объект фрагмент по-прежнему сохраняется после yield chunk.

SECONDLY, идя дальше в рассуждениях, сбор ваших двух цитат приводит к выводу, что вы думаете, потому что chunk больше не используется после команды yield chunk в Радиальный код, на котором не поддерживается ссылка.
Это вопрос логики, ИМО: отсутствие ссылки на объект является условием его освобождения, следовательно, если вы притворяетесь, что память освобождается от объекта, потому что она больше не используется, она эквивалентна притворяться, что память освобождена от объекта, потому что его безработица заставляет intepreter удалять ссылку на него в функции.

Подводя итог:
вы притворяетесь, что в коде Radim chunk больше не используется после yield chunk, тогда больше никаких ссылок на него не будет, тогда..... cpython 2.7 не сделает этого... но pypy 1.6 с по умолчанию gc освобождает память от объекта chunk.

На этом этапе я очень удивлен рассуждениями об источнике этого следствия: это было бы из-за того, что chunkбольше не используется, чтобы pypy 1.6 освободил его. Это рассуждение не так четко выражено вами, но без него я бы нашел то, что вы утверждаете в двух цитатах, которые нелогичны и непонятны.

Что мешает мне в этом заключении, и причина, по которой я не согласен со всем этим, заключается в том, что это означает, что pypy 1.6 сможет анализировать код и обнаруживать, что chunk не будет снова использоваться после yield chunk. Я считаю эту идею совершенно невероятной, и я бы хотел:

  • чтобы объяснить, что вы точно думаете обо всем этом. Где я ошибаюсь в понимании ваших идей?

  • сказать, если у вас есть доказательство того, что, по крайней мере, pypy 1.6, не содержит ссылки на chunk, когда он больше не используется.
    Проблема начального кода Radim заключается в том, что память слишком сильно поглощалась настойчивостью объекта chunk из-за того, что его ссылка все еще сохраняется внутри функции генератора: это был косвенный симптом существования такого постоянная ссылка внутри.
    Наблюдали ли вы подобное поведение с pypy 1.6? Я не вижу другого способа показать остальную ссылку внутри генератора, так как, согласно вашей цитате № 2, любое использование фрагмента после yield chunk достаточно, чтобы вызвать поддержку ссылку на него. Это проблема, подобная одной в квантовой механике: факт измерения скорости частицы изменяет ее скорость.....

Ответ 4

Определенная функция grouper имеет артефакт создания расточительных дубликатов, потому что вы обернули функцию, не влияющую на itertools.islice. Решение заключается в удалении избыточного кода.

Я думаю, что есть уступки языкам C-производных, которые не являются питонами и вызывают избыточные издержки. Например, у вас есть

i = iter(iterable)
itertools.islice(i)

Почему существует i? iter не будет выставлять неизменяемый в итерируемый, таких отливок нет. Если задано значение non-iterable, обе эти строки генерируют исключение; первый не защищает второй.

islice будет happliy действовать как итератор (хотя может дать экономике, что инструкция yield не будет. У вас слишком много кода: grouper, вероятно, не требуется.