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
, вероятно, не требуется.