python - 链接发电机被认为是有害的?

标签 python memory generator cpython pypy

我声称:Python 中的链接生成器内存效率低下,并且使它们无法用于某些类型的应用程序。如果可能,请证明我是错的。

首先,一个没有生成器的非常简单直接的例子:

import gc

def cocktail_objects():
    # find all Cocktail objects currently tracked by the garbage collector
    return filter(lambda obj: isinstance(obj, Cocktail), gc.get_objects())

class Cocktail(object):
    def __init__(self, ingredients):
        # ingredients represents our object data, imagine some heavy arrays
        self.ingredients = ingredients
    def __str__(self):
        return self.ingredients
    def __repr__(self):
        return 'Cocktail(' + str(self) + ')'

def create(first_ingredient):
    return Cocktail(first_ingredient)

def with_ingredient(cocktail, ingredient):
    # this could be some data transformation function
    return Cocktail(cocktail.ingredients + ' and ' + ingredient)

first_ingredients = ['rum', 'vodka']

print 'using iterative style:' 
for ingredient in first_ingredients:
    cocktail = create(ingredient)
    cocktail = with_ingredient(cocktail, 'coke')
    cocktail = with_ingredient(cocktail, 'limes')
    print cocktail
    print cocktail_objects()

这按预期打印:
rum and coke and limes
[Cocktail(rum and coke and limes)]
vodka and coke and limes
[Cocktail(vodka and coke and limes)]

现在让我们使用迭代器对象使鸡尾酒转换更容易组合:
class create_iter(object):
    def __init__(self, first_ingredients):
        self.first_ingredients = first_ingredients
        self.i = 0

    def __iter__(self):
        return self

    def next(self):
        try:
            ingredient = self.first_ingredients[self.i]
        except IndexError:
            raise StopIteration
        else:
            self.i += 1
            return create(ingredient)

class with_ingredient_iter(object):
    def __init__(self, cocktails_iter, ingredient):
        self.cocktails_iter = cocktails_iter
        self.ingredient = ingredient

    def __iter__(self):
        return self

    def next(self):
        cocktail = next(self.cocktails_iter)
        return with_ingredient(cocktail, self.ingredient)

print 'using iterators:'
base = create_iter(first_ingredients)
with_coke = with_ingredient_iter(base, 'coke')
with_coke_and_limes = with_ingredient_iter(with_coke, 'limes')
for cocktail in with_coke_and_limes:
    print cocktail
    print cocktail_objects() 

输出与之前相同。

最后,让我们用生成器替换迭代器以摆脱样板:
def create_gen(first_ingredients):
    for ingredient in first_ingredients:
        yield create(ingredient)

def with_ingredient_gen(cocktails_gen, ingredient):
    for cocktail in cocktails_gen:
        yield with_ingredient(cocktail, ingredient)

print 'using generators:'
base = create_gen(first_ingredients)
with_coke = with_ingredient_gen(base, 'coke')
with_coke_and_limes = with_ingredient_gen(with_coke, 'limes')

for cocktail in with_coke_and_limes:
    print cocktail
    print cocktail_objects()

然而,这会打印:
rum and coke and limes
[Cocktail(rum), Cocktail(rum and coke), Cocktail(rum and coke and limes)]
vodka and coke and limes
[Cocktail(vodka), Cocktail(vodka and coke), Cocktail(vodka and coke and limes)]

这意味着在生成器链中,该链中所有当前产生的对象都保留在内存中并且不会被释放,即使不再需要先前链位置中的对象。结果:高于必要的内存消耗。

现在,问题是:为什么生成器会一直持有它们产生的对象直到下一次迭代开始?显然,生成器中不再需要这些对象,并且可以释放对它们的引用。

我在我的一个项目中使用生成器在一种管道中转换大量数据(数百兆字节的 numpy 数组)。但是正如你所看到的,这在内存方面是非常低效的。我正在使用 Python 2.7。如果这是在 Python 3 中修复的行为,请告诉我。否则,这是否符合错误报告的条件?最重要的是,除了如图所示重写之外,还有其他解决方法吗?

变通方法 1 :
print 'using imap:'
from itertools import imap
base = imap(lambda ingredient: create(ingredient), first_ingredients)
with_coke = imap(lambda cocktail: with_ingredient(cocktail, 'coke'), base)
with_coke_and_limes = imap(lambda cocktail: with_ingredient(cocktail, 'limes'), with_coke)

for cocktail in with_coke_and_limes:
    print cocktail
    print gc.collect()
    print cocktail_objects()

显然,这只有在“产量”之间不需要保持状态时才可用。在示例中就是这种情况。

初步结论:如果你使用迭代器类,那么你决定要保持什么状态。如果您使用生成器,Python 会隐式地决定要保留的状态。如果您使用 itertools.imap你不能保持任何状态。

最佳答案

您的 with_coke_and_limes在执行过程中的某个点产生 yield 。此时,该函数有一个名为 cocktail 的局部变量。 (来自它的 for 循环)它指的是生成器嵌套中下一步的“中间”鸡尾酒(即“朗姆酒和可乐”)。仅仅因为生成器在那个时候产生并不意味着它可以扔掉那个对象。 with_ingredient_gen的执行在那一点被挂起,在这一点上局部变量 cocktail仍然存在。该函数在恢复后可能需要稍后引用它。没有什么可以说 yield必须是您 for 中的最后一件事循环,或者只有一个 yield .你可以写 with_ingredient_gen像这样:

def with_ingredient_gen(cocktails_gen, ingredient):
    for cocktail in cocktails_gen:
        yield with_ingredient(cocktail, ingredient)
        yield with_ingredient(cocktail, "another ingredient")

如果 Python 扔掉 cocktail在第一次产生之后,当它在下一次迭代中恢复生成器并发现它需要cocktail 时,它会做什么?再次反对第二次 yield ?

这同样适用于链中的其他生成器。一旦您提前 with_coke_and_limes调制鸡尾酒,with_cokebase也被激活然后暂停,并且它们有指代自己的中间鸡尾酒的局部变量。如上所述,这些函数不能删除它们所引用的对象,因为它们在恢复后可能需要它们。

生成器函数必须对一个对象有某种引用才能产生它。并且它必须在它产生后保留该引用,因为它在产生后立即暂停,但它无法知道一旦恢复它是否需要引用。

请注意,您在第一个示例中没有看到中间对象的唯一原因是您在每个连续的鸡尾酒中覆盖了相同的局部变量,从而允许释放较早的鸡尾酒对象。如果在你的第一个代码片段中你这样做:
for ingredient in first_ingredients:
    cocktail = create(ingredient)
    cocktail2 = with_ingredient(cocktail, 'coke')
    cocktail3 = with_ingredient(cocktail, 'limes')
    print cocktail3
    print cocktail_objects()

...然后你会看到在这种情况下打印的所有三个中间鸡尾酒,因为每个现在都有一个单独的局部变量引用它。您的生成器版本将这些中间变量中的每一个拆分为单独的函数,因此您不能用“派生”鸡尾酒覆盖“父”鸡尾酒。

如果您有一个深度嵌套的生成器序列,每个生成器都在内存中创建大对象并将它们存储在局部变量中,那么这可能会导致问题,这是对的。然而,这并不是一个普遍的情况。在这种情况下,您有几种选择。一个是在第一个示例中以“平面”迭代样式执行操作。

另一种选择是编写中间生成器,这样它们实际上不会创建大对象,而只是“堆叠”这样做所需的信息。例如,在您的示例中,如果您不想要中间 Cocktail对象,不要创建它们。不是让每个生成器创建鸡尾酒,然后让下一个生成器提取前一个鸡尾酒的成分,而是让生成器只传递成分,并使用一个最终生成器将堆叠的成分组合在一起,并在最后创建一种鸡尾酒。

很难确切地说明如何为您的实际应用程序执行此操作,但它可能是可能的。例如,如果您处理 numpy 数组的生成器正在执行诸如加法、减法、转置等操作,您可以传递描述要执行的操作的“增量”,而无需实际执行。与其使用中间生成器,例如将数组乘以 3 并生成数组,不如让它生成某种指标,例如“*3”(或者甚至可能是一个进行乘法的函数)。然后你的最后一个生成器可以迭代这些“指令”并在一个地方执行所有操作。

关于python - 链接发电机被认为是有害的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26186426/

相关文章:

python - AMPL:数据集和规范中的一个大集合

c# - 128 GB Ram x64 cpu 内存不足问题

python - 如何调整for循环的重复次数?

c - 静态变量的地址相同,但局部变量的地址不同

iOS FMDB Sqlite 包装器。内存不足

python - 生成总和可被 n 整除的随机数列表

python - 将生成器对象转换为列表

Python - wxPython 中的多态性,出了什么问题?

python - 在 Python 中替换和添加文本文件中的文本

python - pyqtgraph:保存/导出 3d 图