Я утверждаю: цепные генераторы в 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
, вы не можете сохранить какое-либо состояние.
Что касается python 3: «yield from» - это более эффективный способ создания цепочек генераторов, да. – roippi
@roippi более эффективен в смысле предотвращения таких эффектов памяти или просто более эффективен для написания? – letmaik
'yield from' не собирается рассматривать проблемы использования памяти, которые вы поднимаете в своем вопросе, AFAIK.Оптимизации, которые допускают «из-за», связаны со скоростью, согласно [ее PEP] (http://legacy.python.org/dev/peps/pep-0380/#optimisations). – dano