连锁发电机被认为是有害的?

2024-05-14 15:58:50 发布

您现在位置:Python中文网/ 问答频道 /正文

我声称: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()

这将按预期打印:

^{pr2}$

现在让我们使用迭代器对象使鸡尾酒变换更易于组合:

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数组)。但正如你所看到的,这是非常低效的内存。我使用的是python2.7。如果这是在python3中修复的行为,请告诉我。否则,这是否符合bug报告的条件?最重要的是,除了如图所示重写之外,还有什么变通方法吗?在


围绕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,则无法保持任何状态。在


Tags: andselfreturndefcreatewithfirstprint
1条回答
网友
1楼 · 发布于 2024-05-14 15:58:50

您的with_coke_and_limes在其执行过程中的某一点产生。此时,函数有一个名为cocktail(来自它的for循环)的局部变量,它引用生成器嵌套中下一步的“中间”鸡尾酒(即“rum and coke”)。仅仅因为生成器在这一点上屈服并不意味着它可以扔掉那个物体。此时,with_ingredient_gen的执行被挂起,并且在该点上局部变量{}仍然存在。函数可能需要在它恢复之后引用它。没有什么能说明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,那么当它在下一次迭代中恢复生成器并发现它再次需要{}对象来获得第二个结果时,它会怎么做?在

这同样适用于链条中的其他发电机。一旦你推进with_coke_and_limes来制造鸡尾酒,with_coke和{}也会被激活,然后被暂停,它们的局部变量是指它们自己的中间鸡尾酒。如上所述,这些函数不能删除它们引用的对象,因为它们在恢复后可能需要它们。在

生成函数对某个对象有某种引用,以便生成它。它必须在它产生引用之后保留它,因为它在它产生之后立即被挂起,但是它不知道一旦它被恢复它是否需要这个引用。在

请注意,在第一个示例中没有看到中间对象的唯一原因是,您对每个连续的cockit都重写了相同的局部变量,从而允许释放前面的cockit对象。如果在第一个代码片段中改为执行以下操作:

^{pr2}$

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

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

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

很难说如何为您的实际应用程序做到这一点,但这可能是可能的。例如,如果在numpy数组上工作的生成器正在执行诸如加法、减法、转置等操作,则可以传递“delta”来描述不实际执行操作的操作。与其有一个中间生成器,比方说,将一个数组乘以3并生成数组,不如让它生成某种类似“*3”的指示符(或者甚至可能是一个进行乘法的函数)。最后一个生成器可以迭代这些“指令”,并在一个地方执行所有操作。在

相关问题 更多 >

    热门问题