Python:产量
我如何从发生器产生一个对象并立即忘记它,以免它占用内存?
例如,在以下函数中:
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
,因为它不会被进一步使用,只会消耗内存,即使所有外部引用都消失了。
编辑 :从python.org使用标准的Python 2.5 / 2.6 / 2.7。
解决方案 (由@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
如果你真的想要获得这个功能,我想你可以使用一个包装器:
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()
所做的基本相同;)
在yield chunk
,变量值从不会在函数中再次使用,所以一个好的解释器/垃圾回收器已经为垃圾收集释放了chunk
(注意:cpython 2.7似乎没有这样做,pypy 1.6使用默认的gc)。 因此,除了代码示例以外,您不必更改任何内容,但缺少grouper
的第二个参数。
请注意,垃圾收集在Python中是非确定性的。 空垃圾回收器根本不收集任何对象,是一个完美有效的垃圾回收器。 从Python手册:
对象从不明确销毁; 然而,当他们无法到达时,他们可能会被垃圾收集。 允许实现推迟垃圾收集或完全忽略垃圾收集 - 只要没有收集到仍然可以访问的对象,实施质量如何实施垃圾收集就成了一个问题。
因此,如果没有指定Python实现和垃圾收集器,Python程序是否确实或“不占用内存”是无法决定的。 给定一个特定的Python实现和垃圾回收器,你可以使用gc
模块来测试对象是否被释放。
这就是说,如果你真的不想从函数中引用(不一定意味着对象将被垃圾收集),下面是如何做到这一点:
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的包装器)的数据结构。
@ 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
,无法从该函数访问存储在chunk中的值。 因此,该函数不包含任何对该对象的引用
(顺便说一句,我不会写,所以'因为')
我认为这个肯定#1是有争议的。
事实上,我相信这是错误的。 但是,如果我们在回答开始时考虑到你所说的话,那么你的假装就有一个微妙之处,而不仅仅是在这个引用中,而是在全球范围内。
让我们把事情整理一下。
下面的代码似乎证明了你的肯定的相反之处“在yield块之后,没有办法从该函数访问存储在chunk中的值。”
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)。
这次你不会说这个函数在yield chunk
chunk后面没有更多chunk的引用,你说它的值在while
循环的下一个循环中的chunk更新之前不会再使用 。 没错,在Radim的代码中,在下一轮的指令chunk = list(itertools.islice(i, int(chunksize)))
中重新分配标识符'chunk'之前,不再使用对象块循环。
。
这个引用中的肯定#2与前面的#1不同,有两个逻辑结果 :
首先 ,我上面的代码不能假装向像你这样的人证明,确实有办法在yield chunk
指令之后访问chunk的值。
因为我上面的代码中的条件与您确认相反的条件不同,也就是说:在Radim的代码中,您所说的代码块在下一回合之前确实不会再使用。
然后,我们可以假装这是因为我在上面的代码中使用了块 (指令print 'end of turn',id(chunk),'n'
, print 'new turn ',id(chunk)
, last = chunk[-1]
确实使用它),碰巧在对象块的引用仍然保留在yield chunk
。
其次 ,在推理走得更远,收集你的两个报价导致得出结论,你认为这是因为大块不再是在使用后yield chunk
没有提及维持它在拉迪姆的代码指令。
这是一个逻辑问题,IMO:缺少对象的引用是它释放的条件,因此如果你假装内存从对象中释放出来,因为它不再被使用,它相当于假装内存已被释放因为它的失业使得解释者在函数中删除对它的引用。
我总结:
你假装在Radim的代码中, chunk在yield chunk
之后不再被使用,那么就没有更多的引用,然后..... cpython 2.7不会这么做......但是使用默认gc的pypy 1.6将内存从对象块 。
在这一点上,我对这个后果的推论非常惊讶:这可能是因为大块不再被使用,pypy 1.6会释放它。 这个推理并没有被你清楚地表达出来,但是如果没有它,我会发现你在两个引用中声称是不合逻辑的和不可理解的。
这个结论让我感到困惑,并且我不同意这一点,因为它意味着pypy 1.6将能够分析代码并检测该块在yield chunk
之后不会再被使用。 我觉得这个想法完全不可思议,我希望你:
解释你究竟想到了什么。 我的理解错在哪里?
如果你有证据证明至少pypy 1.6在不再使用时不会引用块 。
Radim的初始代码的问题在于内存过多地被对象块的持久性所占用,因为它的引用仍然保存在生成器函数中:这是内部存在这种持久引用的间接症状。
你有没有观察到类似pypy 1.6的行为? 我看不出证据来换个说法剩余参考发生器内,因为根据你的报价#2,之后的任何使用大块的yield chunk
都足以引起对它的引用的崇尚。 这与量子力学中的问题类似:测量粒子速度的事实改变了它的速度.....
上一篇: Python: yield