itertools.chain()是否允许从消耗的项目中释放内存

时间:2017-02-02 10:21:51

标签: python memory iteration itertools

我试图通过批量运行大型查询来减少Django应用程序中的内存使用量。尽管我认为自己很聪明,但内存使用量一直在增长,直到最终这个过程被杀死。我目前有一个理论认为查询集不是垃圾收集的,我想知道它是否与我如何批量查询有关。

def grouper(iterable, n, fillvalue=None):
    "Collect data into fixed-length chunks or blocks"
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

def get_data(pks):
    groups = list(grouper(pks, batch_size))
    querysets = [_queryset(pks) for pks in groups]
    return itertools.chain(*querysets)

for item in get_data([..big list of primary keys..]):
    process(item)

假设在get_data函数中生成了3个查询集。当我使用了第一个查询集中的所有项目时,该查询集是否会被释放?或者我是否仍然在技术上从链中获得它的参考?

我不确定这里是否存在内存(它可能是数据库驱动程序,Django本身内部的内容等),但它似乎是一个不错的候选者。是否有任何好的工具来测量对象类型的内存使用?这个特殊的代码在Python 2上运行(现在)。

我应该注意到我是从ipython shell运行它,只是因为这很重要。

编辑:

看来chain在这里不负责任。我添加了一些代码来打印每个类的对象计数,并且Model对象的数量保持不变。

import gc

def get_object_counts():
    from collections import Counter
    classes = []
    for obj in gc.get_objects():
        if hasattr(obj, '__class__'):
            classes.append(str(obj.__class__))
    return Counter(classes)

然后按特定间隔(批量大小):

print(get_object_counts().most_common(30))

为了完成起见,这里排名前9位。我认为主要罪魁祸首是django.db.models.base.ModelState,它会不断增长而不会被收集。

首先:

("<type 'dict'>", 59184)
("<type 'list'>", 48710)
("<type 'function'>", 48300)
("<type 'tuple'>", 38920)
("<type 'cell'>", 10203)
("<type 'weakref'>", 9957)
("<type 'set'>", 7230)
("<type 'type'>", 5947)
("<class 'django.db.models.base.ModelState'>", 4682)

第二

("<type 'dict'>", 59238)
("<type 'list'>", 48730)
("<type 'function'>", 48315)
("<type 'tuple'>", 38937)
("<type 'cell'>", 10207)
("<type 'weakref'>", 9959)
("<type 'set'>", 7230)
("<type 'type'>", 5950)
("<class 'django.db.models.base.ModelState'>", 4696)

2 个答案:

答案 0 :(得分:3)

我自己也看到了类似的行为,似乎itertools.chain(和itertools.chain.from_iterable)保留对作为参数传递的任何内容的引用,直到它们停止迭代为止。我希望这就是为什么你的django查询集以及它们的缓存结果不会被垃圾收集。这似乎是python2和python3上的行为,也是您从任何用户定义的python函数中看到的行为(请参阅How to delete a function argument early?)。也许在诸如itertools之类的C语言库中的函数可以更自由地在退出之前删除对参数的引用,但显然他们不会选择这样做。

作为一种变通方法,您可以使用itertools.chainiter本身将各个参数包装到迭代器中的itertools.chain。看来,一旦这些单独的迭代器耗尽,它们就会删除它们对底层迭代的引用,并允许它被垃圾收集。

最后要注意的是,即使完全消耗结果也不足以释放内存 - 必须在迭代器(或sub)使用的内存之前再次将控件返回到迭代器(或链中的任何子迭代器) -iterator)发布。同样,这是你可能期望的普通python函数。

下面的代码显示了这一切:

from __future__ import print_function
import itertools
import gc

def print_whats_left_after(num, numbers_iter):
    """ Read three numbers and print what pairs haven't been gc'd """
    for _ in range(num):
        next(numbers_iter, None)
    gc.collect()
    # Print integer pairs that were not garbage collected
    print(sorted([o for o in gc.get_objects()
                  if isinstance(o, list) and len(o) == 2 and
                  all(isinstance(i, int) for i in o)]))

print_whats_left_after(2, itertools.chain([1, 2], [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain([1, 2], [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(4, itertools.chain([1, 2], [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(5, itertools.chain([1, 2], [3, 4]))
# -> []

print_whats_left_after(2, itertools.chain.from_iterable([[1, 2], [3, 4]]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain.from_iterable([[1, 2], [3, 4]]))
# -> [[1, 2], [3, 4]]

print_whats_left_after(2, itertools.chain(itertools.chain([1, 2]), [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain(itertools.chain([1, 2]), [3, 4]))
# -> [[3, 4]]  # [1, 2] was gc'd!!!

print_whats_left_after(2, itertools.chain(iter([1, 2]), [3, 4]))
# -> [[1, 2], [3, 4]]
print_whats_left_after(3, itertools.chain(iter([1, 2]), [3, 4]))
# -> [[3, 4]]  # [1, 2] was gc'd!!!
print_whats_left_after(4, itertools.chain(iter([1, 2]), [3, 4]))
# -> [[3, 4]]
print_whats_left_after(5, itertools.chain(iter([1, 2]), [3, 4]))
# -> []

def arg_clobberer(arg):
    arg = None
    yield
print_whats_left_after(0, arg_clobberer([1, 2]))
# -> [[1, 2]]
print_whats_left_after(1, arg_clobberer([1, 2]))
# -> []

def arg_deleter(arg):
    del arg
    yield
print_whats_left_after(0, arg_deleter([1, 2]))
# -> [[1, 2]]
print_whats_left_after(1, arg_deleter([1, 2]))
# -> []

希望这有帮助!

答案 1 :(得分:0)

不确定但是你可以通过将它们转换为iterables / generators来避免创建临时列表(并在源处修复分配问题):

def get_data(pks):
    groups = grouper(pks, batch_size)  # turn off explicit list conversion
    querysets = (_queryset(pks) for pks in groups)  # gencomp not listcomp
    return itertools.chain.from_iterable(querysets)  # nicer with "from_iterable"