2014-10-03 11 views
4

I 주장 : 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() 

출력은 이전과 동일합니다.

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)] 

이 발전기의 체인의 모든 현재 개체를 굴복을 의미합니다 :

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() 

이 그러나 인쇄 : 마지막으로

,의 보일러 플레이트 없애 발전기 반복자를 대체 할 수 이전 체인 위치의 체인이 더 이상 필요하지 않더라도 체인은 메모리에 남아 있고 해제되지 않습니다. 결과 : 필요한 메모리 소비보다 높습니다.

이제 질문은 다음과 같습니다. 왜 생성기는 다음 반복이 시작될 때까지 생성하는 객체를 보유하고 있습니까? 분명히 물체는 발전기에서 더 이상 필요하지 않으며 그것들에 대한 참조는 해제 될 수 있습니다.

저는 일종의 파이프 라인에서 무거운 데이터 (수백 메가 바이트의 뭉툭한 배열)를 변환하기 위해 제 프로젝트에서 발전기를 사용하고 있습니다. 그러나 보시다시피 이것은 매우 비효율적 인 메모리입니다. 파이썬 2.7을 사용하고 있습니다. 이것이 파이썬 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() 

은 분명히이 만 가능한 것입니다. 예에서 이것은 사실입니다.

예비 결론 : 반복자 클래스를 사용하는 경우 은 유지할 상태를 결정합니다. 생성자를 사용하는 경우 파이썬은 유지할 상태를 암시 적으로 결정합니다. itertools.imap을 사용하면 상태를 유지할 수 없습니다.

+1

python 3의 경우 : 'yield from'은 생성기를 체인화하는보다 효율적인 방법입니다. – roippi

+0

@roippi 이러한 메모리 효과를 방지하거나 쓰기에 더 효율적이라는 의미에서 더 효율적입니까? – letmaik

+0

'yield from'는 AFAIK라는 질문에서 제기하는 메모리 사용 문제를 해결하지 않을 것입니다.'yield from'이 허용하는 최적화는 [해당 PEP] (http://legacy.python.org/dev/peps/pep-0380/#optimisations)에 따라 속도와 관련이 있습니다. – dano

답변

4

귀하의 with_coke_and_limes은 실행 중 특정 시점에서 발생합니다. 이 시점에서 함수에는 생성자 중첩 (즉, "럼 및 콜라")에서 다음 단계의 "중간"칵테일을 나타내는 cocktail (for 루프) 로컬 변수가 있습니다. 그 시점에서 발전기가 산출량을 산출했다고해서 그것이 그 물체를 버릴 수 있다는 것을 의미하지는 않습니다. with_ingredient_gen의 실행이 그 시점에서 일시 중지되고 해당 시점에 로컬 변수 cocktail이 여전히 존재합니다. 나중에 다시 시작한 후에 함수를 참조해야 할 수도 있습니다.yieldfor 루프의 마지막 요소 여야하거나, 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") 

파이썬이 첫 번째 항복 후 멀리 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 오브젝트를 원하지 않으면 오브젝트를 작성하지 마십시오. 각 발전기에 칵테일을 만들고 다음 발전기를 사용하여 이전 칵테일의 재료를 추출하는 대신 발전기에 성분만을 전달하고 쌓인 재료를 결합하여 마지막에 단 하나의 칵테일을 만드는 최종 발전기 1 개를 보유하십시오.

실제 응용 프로그램에서이 작업을 수행하는 방법을 정확하게 말하는 것은 어렵지만 가능할 수도 있습니다. 예를 들어, numpy 배열에서 작업하는 생성자가 이것을 추가하거나 빼거나 전치 등을 수행하는 경우 실제로 수행하지 않고 수행 할 작업을 설명하는 "델타"를 전달할 수 있습니다. 중간 생성기를 갖는 대신에 배열을 3으로 곱하고 배열을 산출하면 "* 3"과 같은 지시자 (또는 심지어 곱셈을 수행하는 함수조차도)를 생성하게하십시오. 그런 다음 마지막 생성기는 이러한 "지침"을 반복하여 한 곳에서 작업을 수행 할 수 있습니다.

+0

기본적으로 파이썬은 인터프리터 언어이기 때문에 "앞을 내다 보지"않고 중간 변수가 더 이상 필요하지 않다는 것을 알 수 있습니다. – letmaik

+0

@neo : 본질적으로 그렇습니다. – BrenBarn

+0

@neo : 가능한 해결 방법에 대한 정보를 추가하도록 편집했습니다. – BrenBarn