链式发电机被认为有害吗?

时间:2014-10-03 20:35:10

标签: 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)]

这意味着在一系列生成器中,该链中所有当前生成的对象都会留在内存中并且不会被释放,即使不再需要早期链中的那些。结果:高于必要的内存消耗。

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

我在我的一个项目中使用生成器来在一种管道中转换繁重的数据(数百兆字节的数组)。但正如你所看到的那样,这在记忆方面是非常低效的。我使用的是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,则无法保留任何状态。

1 个答案:

答案 0 :(得分:4)

你的with_coke_and_limes在执行中的某个时刻屈服。此时,该函数有一个名为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个对象?

这同样适用于链中的其他发电机。一旦您提前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”(或者甚至可能是执行乘法的函数)。然后你的最后一个生成器可以迭代这些“指令”并在一个地方执行所有操作。