Python: yield与删除

16 投票
5 回答
14449 浏览
提问于 2025-04-17 00:00

我想知道如何从一个生成器中返回一个对象,然后立即忘记它,这样就不会占用内存。

比如,在下面这个函数中:

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提出的):把结果放在一个(小的)可变对象中,然后匿名返回这个chunk,只留下那个小容器:

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

5 个回答

2

@ 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' 的对象仍然存在,并且消耗了大量内存,因为这个对象和生成器函数内部的标识符 'chunk' 之间仍然有绑定关系?

请原谅我现在问你这个显而易见的问题。
不过,由于讨论中曾经有些混乱,我希望你能确认我现在是否正确理解了你的问题。

.

@ phihag

你在评论中写道:

1)
yield chunk 之后,无法从这个函数访问存储在 chunk 中的值。因此,这个函数不持有任何对相关对象的引用。

(顺便说一句,我不会用 因此,而是用 '因为')

我认为这个说法 #1 是有争议的。
事实上,我相信它是错误的。但你所说的内容有一些微妙之处,不仅仅在这段引用中,而是整体上,如果我们考虑你在回答开头所说的内容。

让我们按顺序来。

以下代码似乎证明了你的说法 "在 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)
yield chunk 之后,变量值在函数中不再使用,因此一个好的解释器/垃圾回收器会立即将 chunk 释放以进行垃圾回收(注意:cpython 2.7 似乎没有这样做,而 pypy 1.6 在默认的垃圾回收下会这样做)。

这次你没有说在 yield chunk 之后函数不再持有 chunk 的引用,你说它的值在下一个 chunk 被重新赋值之前不再使用。这没错,在 Radim 的代码中,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 的代码中 chunkyield chunk 指令之后不再使用,所以没有对它的引用被保留。
在我看来,这是逻辑问题:对一个对象的引用缺失是其被释放的条件,因此如果你认为内存是因为对象不再使用而被释放的,那就等于认为内存是因为其不再使用而导致解释器删除了对它的引用。

我总结一下:
你认为在 Radim 的代码中,chunkyield chunk 之后不再使用,因此没有对它的引用被保留,然后..... cpython 2.7 不会这样做......但 pypy 1.6 在默认的垃圾回收下会释放 chunk 对象的内存。

在这一点上,我对这个结论的推理感到非常惊讶:似乎是因为 chunk 不再使用,pypy 1.6 才会释放它。这个推理你没有明确表达,但如果没有它,我会觉得你在两个引用中所说的内容不合逻辑且难以理解。

让我困惑的是,这个结论暗示 pypy 1.6 能够分析代码并检测到 chunkyield chunk 之后不会再使用。我觉得这个想法完全不可思议,我希望你:

  • 解释一下你对这一切的确切看法。我在理解你的想法上错在哪里?

  • 告诉我你是否有证据表明,至少在 pypy 1.6 中,当 chunk 不再使用时,它不会持有对 chunk 的引用。
    Radim 的初始代码的问题在于,由于在生成器函数内部仍然持有对对象 chunk 的引用,导致内存消耗过多:这间接表明存在这样的持久引用。
    你观察到 pypy 1.6 有类似的行为吗?我看不到其他方法来证明生成器内部仍然存在引用,因为根据你的引用 #2,在 yield chunk 之后对 chunk 的任何使用都足以触发对它的引用的保持。这是一个类似于量子力学的问题:测量一个粒子的速度会改变它的速度.....

11

在执行了 yield chunk 之后,变量 value 在这个函数里就再也没有被使用过了,所以一个好的解释器或者垃圾回收器会把 chunk 释放掉,准备进行垃圾回收(注意:cpython 2.7 似乎没有这样做,而 pypy 1.6 的默认垃圾回收器是会的)。因此,你只需要修改你的代码示例,因为它缺少了 grouper 的第二个参数。

需要注意的是,Python 的垃圾回收是不可预测的。所谓的 null 垃圾回收器,根本不回收任何空闲对象,这也是一种完全有效的垃圾回收器。根据 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 的包装器

4

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

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()做的事情是一样的;)

撰写回答