2014-10-03 7 views
4

Я утверждаю: цепные генераторы в 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

Что касается python 3: «yield from» - это более эффективный способ создания цепочек генераторов, да. – roippi

+0

@roippi более эффективен в смысле предотвращения таких эффектов памяти или просто более эффективен для написания? – letmaik

+0

'yield from' не собирается рассматривать проблемы использования памяти, которые вы поднимаете в своем вопросе, AFAIK.Оптимизации, которые допускают «из-за», связаны со скоростью, согласно [ее PEP] (http://legacy.python.org/dev/peps/pep-0380/#optimisations). – dano

ответ

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_coke и base также активированы, а затем приостановлены, и у них есть локальные переменные, относящиеся к их собственным промежуточным коктейлям. Как описано выше, эти функции не могут удалить объекты, на которые они ссылаются, поскольку они могут понадобиться после возобновления.

Функция генератора имеет, чтобы иметь ссылку на объект, чтобы получить его. И он должен сохранить эту ссылку после ее получения, потому что она приостанавливается сразу же после ее выхода, но не может знать, понадобится ли эта ссылка после ее возобновления.

Обратите внимание, что единственная причина, по которой вы не видели промежуточных объектов в первом примере, состоит в том, что вы переписывали одну и ту же локальную переменную с каждым последующим коктейлем, позволяя освобождать более ранние объекты коктейлей. Если в первом фрагменте кода вы вместо этого:

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» (или, возможно, даже функцию, выполняющую умножение). Затем ваш последний генератор может перебирать эти «инструкции» и выполнять операции в одном месте.

+0

Итак, в основном вы говорите, что, поскольку Python - это интерпретируемый язык, он не «смотрит в будущее» и выясняет, что промежуточная переменная больше не нужна. – letmaik

+0

@neo: По сути, да. – BrenBarn

+0

@neo: Я отредактировал, чтобы добавить информацию о возможных обходных решениях. – BrenBarn

Смежные вопросы