Python:yield-and-d

2024-06-16 16:24:07 发布

您现在位置: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

Tags: 对象函数内存inifdefnotbe
3条回答

@半径

有几点使我在这方面感到困惑。我意识到我错过了理解的基础:什么是你的问题。

现在我想我已经明白了,请你确认。

我会那样代表你的代码

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
.....................
........

你的问题是,即使是在
#删除后,代码继续执行,需要时间来执行
以及
在B之前,
已删除名称为“A”的对象仍然存在并占用大量内存,因为此对象与生成器函数中的标识符“chunk”之间仍然存在绑定?

请原谅我问你这个显而易见的问题。
但是,由于一次在线程中出现了某种混乱,我希望您确认我现在已经正确地理解了您的问题。

是的。

@菲哈格

你在评论中写道:

1)
After the yield chunk, there is no way to access the value stored in chunk from this function. Therefore, this function does not hold any references to the object in question

(顺便说一句,我不会写所以,而是写'because')

我认为这种肯定是有争议的 事实上,我确信这是假的。但是,如果我们也考虑到你在回答的开头所说的话,你的伪装就有微妙之处,不仅仅是在这段引语中,而是在全球范围内。

让我们把东西收拾好。

下面的代码似乎证明了与您的肯定相反,在yield chunk之后,没有办法从这个函数访问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)
After yield chunk, the variable value is never used again in the function, so a good interpreter/garbage collector will already free chunk for garbage collection (note: cpython 2.7 seems not do this, pypy 1.6 with default gc does).

这一次,您不会说函数在yield chunk之后不再持有chunk的引用,而是说在while循环的下一轮中更新chunk之前,它的值再次是未使用。没错,在Radim的代码中,在下一轮循环中,在指令chunk = list(itertools.islice(i, int(chunksize)))中重新分配标识符“chunk”之前,对象“chunk”不会再次使用。

是的。

这段引文中的肯定2不同于前面的肯定1,有两个逻辑结果:

首先,我上面的代码无法向像您这样的人严格证明,在yield chunk指令之后确实有方法访问chunk的值。
因为我上面代码中的条件与您确认相反的条件不同,也就是说:在您所说的Radim代码中,对象chunk在下一回合之前确实没有再次使用。
然后,我们可以假设这是因为在我上面的代码中使用了chunk(指令print 'end of turn',id(chunk),'\n'print 'new turn ',id(chunk)last = chunk[-1]确实使用了它),在yield chunk之后,对对象chunk的引用仍然保持。

其次,进一步推理,收集您的两个引文,得出结论,您认为这是因为在Radim代码中的yield chunk指令之后,chunk不再使用,因此没有对它进行任何引用。
这是一个逻辑问题,IMO:没有对对象的引用是其释放的条件,因此如果你假装内存是从对象中释放的,因为它不再被使用,这相当于假设内存从对象中释放出来,因为它的失业使得interpreter在函数中删除对它的引用。

我总结:
假设在Radim的代码中,chunkyield chunk之后不再使用,那么对它的引用就不再是hold,然后。。。。。cpython 2.7不会这么做。。。但是pypy 1.6使用默认的gc从对象块中释放内存。

在这一点上,我对这个结果的原因感到非常惊讶:这是因为rong>chunk不再使用pypy 1.6来释放它。你并没有清楚地表达出这种推理,但没有它,我会发现你在这两个引文中所说的是不合逻辑和不可理解的。

令我困惑的是,这一结论以及我不同意这一切的原因是,它意味着pypy 1.6能够分析代码并检测出在yield chunk之后,不会被再次使用。我觉得这个想法完全难以置信,我希望你:

  • 来解释你到底是怎么想的。我理解你的想法错在哪里?

  • 如果你有证据表明,至少pypy 1.6在不再使用时不包含对块的引用。
    Radim的初始代码的问题是,由于对象的引用仍然保留在生成器函数中,因此对象的持久性消耗了太多内存:这是存在这样一个持久性引用的间接症状。
    你观察过Pypy1.6的类似行为吗?我看不出另一种方法来证明生成器中剩余的引用,因为根据您的引用2,在yield chunk之后使用chunk就足以触发对它的引用。这是一个类似于量子力学的问题:测量一个粒子的速度会改变它的速度。。。。。

yield chunk之后,变量值将不再在函数中使用,因此一个好的解释器/垃圾收集器将已经为垃圾收集释放chunk(注意:cpython 2.7似乎执行此操作,pypy 1.6使用默认gc执行此操作)。因此,除了代码示例之外,您不必更改任何内容,因为它缺少grouper的第二个参数。

注意,垃圾收集在Python中是不确定的。不收集空闲对象的null垃圾收集器是完全有效的垃圾收集器。从Python manual

Objects are never explicitly destroyed; however, when they become unreachable they may be garbage-collected. An implementation is allowed to postpone garbage collection or omit it altogether — it is a matter of implementation quality how garbage collection is implemented, as long as no objects are collected that are still reachable.

因此,如果不指定Python实现和垃圾收集器,就无法确定Python程序是否占用内存。给定特定的Python实现和垃圾收集器,可以使用^{}模块来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's wrapper

如果你真的想得到这个功能,我想你可以使用一个包装器:

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();所做的基本相同)

相关问题 更多 >