파이썬에서 생성기 객체 재설정


153

여러 수율로 반환 된 생성기 객체가 있습니다. 이 생성기를 호출하기위한 준비는 다소 시간이 걸리는 작업입니다. 그래서 발전기를 여러 번 재사용하고 싶습니다.

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

물론 콘텐츠를 간단한 목록으로 복사하는 것을 고려하고 있습니다. 발전기를 재설정하는 방법이 있습니까?

답변:


119

다른 옵션은이 itertools.tee()함수를 사용하여 두 번째 버전의 생성기를 만드는 것입니다.

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

이것은 원래 반복이 모든 항목을 처리하지 않을 수있는 경우 메모리 사용 관점에서 유리할 수 있습니다.


33
이 경우에 수행 할 작업이 궁금하다면 목록의 요소를 캐싱하는 것입니다. 따라서 y = list(y)나머지 코드를 그대로 사용하십시오.
ilya n.

5
tee ()는 내부적으로 데이터를 저장하기 위해 목록을 작성하므로 내 답변에서했던 것과 동일합니다.
nosklo

6
implmentation ( docs.python.org/library/itertools.html#itertools.tee )을 보십시오. 지연로드 전략을 사용하므로 항목은 요청시에만 복사됩니다
Dewfy

11
@Dewfy : 어쨌든 모든 항목을 복사해야하기 때문에 속도느려집니다 .
nosklo

8
예,이 경우 list ()가 더 좋습니다. 티는 당신이 전체 목록을 소비하지 않는 경우에만 유용합니다
중력

148

발전기는 되 감을 수 없습니다. 다음과 같은 옵션이 있습니다.

  1. 생성기 기능을 다시 실행하여 생성을 다시 시작하십시오.

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. 생성기 결과를 메모리 나 디스크의 데이터 구조에 저장하여 다시 반복 할 수 있습니다.

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)

옵션 1 의 단점은 값을 다시 계산한다는 것입니다. CPU 집약적이라면 두 번 계산하게됩니다. 반면에 2 의 단점은 스토리지입니다. 전체 값 목록이 메모리에 저장됩니다. 값이 너무 많으면 실용적이지 않을 수 있습니다.

그래서 당신은 고전적인 메모리 대 프로세싱 트레이드 오프를 가지고 있습니다. 값을 저장하거나 다시 계산하지 않고 생성기를 되 감는 방법을 상상할 수 없습니다.


함수 호출의 서명을 저장하는 방법이있을 수 있습니까? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy : 확인 : def call_my_func () : return FunctionWithYield (param1, param2)
nosklo

@Dewfy "함수 호출의 서명 저장"은 무엇을 의미합니까? 설명해 주시겠습니까? 생성기에 전달 된 매개 변수를 저장하는 것을 의미합니까?
Андрей Беньковский 2016

2
(1)의 또 다른 단점은 FunctionWithYield ()는 비용이 많이들뿐만 아니라 stdin에서 읽는 경우와 같이 다시 계산할 수 없다는 것입니다.
Max

2
@Max가 말한 것을 에코하기 위해 함수의 출력이 호출간에 (또는) 변경 될 수 있다면 (1) 예기치 않은 및 / 또는 바람직하지 않은 결과를 줄 수 있습니다.
Sam_Butler

36
>>> def gen():
...     def init():
...         return 0
...     i = init()
...     while True:
...         val = (yield i)
...         if val=='restart':
...             i = init()
...         else:
...             i += 1

>>> g = gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.send('restart')
0
>>> g.next()
1
>>> g.next()
2

29

아마도 가장 간단한 해결책은 고가의 부품을 객체에 싸서 생성기에 전달하는 것입니다.

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

이렇게하면 비싼 계산을 캐시 할 수 있습니다.

모든 결과를 동시에 RAM에 보관할 수 있으면 list()생성기 결과를 일반 목록으로 구체화하고 이를 사용 하여 작업 할 수 있습니다.


23

오래된 문제에 대해 다른 해결책을 제시하고 싶습니다.

class IterableAdapter:
    def __init__(self, iterator_factory):
        self.iterator_factory = iterator_factory

    def __iter__(self):
        return self.iterator_factory()

squares = IterableAdapter(lambda: (x * x for x in range(5)))

for x in squares: print(x)
for x in squares: print(x)

이와 비슷한 장점 list(iterator)O(1)공간이 복잡하고 그렇다는 것 list(iterator)입니다 O(n). 단점은 반복자에만 액세스 할 수 있지만 반복자를 생성 한 함수는 액세스 할 수없는 경우이 방법을 사용할 수 없다는 것입니다. 예를 들어 다음을 수행하는 것이 합리적으로 보일 수 있지만 작동하지 않습니다.

g = (x * x for x in range(5))

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)

@Dewfy 첫 번째 스 니펫에서 생성기는 "squares = ..."라인에 있습니다. 생성기 표현식은 yield를 사용하는 함수를 호출하는 것과 같은 방식으로 작동하며, 짧은 예제에서 yield를 사용하여 함수를 작성하는 것보다 덜 장황하기 때문에 하나만 사용했습니다. 두 번째 스 니펫에서는 FunctionWithYield를 generator_factory로 사용 했으므로 iter 가 호출 될 때마다 호출됩니다. "for x in y"를 쓸 때마다 호출됩니다.
michaelsnowden

좋은 해결책입니다. 이것은 실제로 상태 저장 반복자 객체 대신 상태 비 저장 반복 가능 객체를 만들므로 객체 자체를 재사용 할 수 있습니다. 반복 가능한 객체를 함수에 전달하고 해당 함수가 객체를 여러 번 사용하는 경우 특히 유용합니다.
Cosyn

5

GrzegorzOledzki의 답변이 충분하지 않다면 send()목표를 달성 하는 데 사용할 수 있습니다. 향상된 생성기 및 수율 표현에 대한 자세한 내용 은 PEP-0342 를 참조하십시오.

업데이트 : 참조하십시오 itertools.tee(). 그것은 위에서 언급 한 메모리 대 프로세싱 트레이드 오프의 일부를 포함하지만 , 생성기 결과를 단지 저장하는 것보다 약간의 메모리를 절약 할 있다 list; 생성기를 사용하는 방법에 따라 다릅니다.


5

생성기가 전달 된 인수와 단계 번호에만 의존한다는 의미에서 생성자가 순수하고 결과 생성기를 다시 시작하려는 경우 다음과 같은 편리한 정렬 스 니펫이 있습니다.

import copy

def generator(i):
    yield from range(i)

g = generator(10)
print(list(g))
print(list(g))

class GeneratorRestartHandler(object):
    def __init__(self, gen_func, argv, kwargv):
        self.gen_func = gen_func
        self.argv = copy.copy(argv)
        self.kwargv = copy.copy(kwargv)
        self.local_copy = iter(self)

    def __iter__(self):
        return self.gen_func(*self.argv, **self.kwargv)

    def __next__(self):
        return next(self.local_copy)

def restartable(g_func: callable) -> callable:
    def tmp(*argv, **kwargv):
        return GeneratorRestartHandler(g_func, argv, kwargv)

    return tmp

@restartable
def generator2(i):
    yield from range(i)

g = generator2(10)
print(next(g))
print(list(g))
print(list(g))
print(next(g))

출력 :

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
0
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1

3

에서 티의 공식 문서 :

일반적으로 다른 반복자가 시작되기 전에 한 반복자가 대부분 또는 모든 데이터를 사용하는 경우 tee () 대신 list ()를 사용하는 것이 더 빠릅니다.

따라서 list(iterable)대신에 사용하는 것이 가장 좋습니다 .


6
무한 발전기는 어떻습니까?
Dewfy

1
속도는 유일한 고려 사항이 아닙니다. list()전체 iterable을 메모리에 넣습니다
Chris_Rands

@Chris_Rands tee()만약 하나의 이터레이터가 모든 값을 소비한다면 그렇게 될 것 tee입니다.
AChampion 2012

2
@Dewfy는 : 무한 발전기를 들어, 사용 아론 Digulla의 솔루션 (ExpensiveSetup 기능은 소중한 데이터를 반환.)
제프 Learman

3

랩퍼 함수를 ​​사용하여 처리 StopIteration

생성기 소진 시점을 추적하는 생성기 생성 함수에 간단한 랩퍼 기능을 작성할 수 있습니다. StopIteration생성자가 반복 끝에 도달하면 예외를 사용하여 수행합니다.

import types

def generator_wrapper(function=None, **kwargs):
    assert function is not None, "Please supply a function"
    def inner_func(function=function, **kwargs):
        generator = function(**kwargs)
        assert isinstance(generator, types.GeneratorType), "Invalid function"
        try:
            yield next(generator)
        except StopIteration:
            generator = function(**kwargs)
            yield next(generator)
    return inner_func

위에서 알 수 있듯이 래퍼 함수가 StopIteration예외를 포착 하면 단순히 함수 호출의 다른 인스턴스를 사용하여 생성기 객체를 다시 초기화합니다.

그런 다음 생성기 제공 함수를 아래와 같이 정의한다고 가정하면 Python 함수 데코레이터 구문을 사용하여 암시 적으로 래핑 할 수 있습니다.

@generator_wrapper
def generator_generating_function(**kwargs):
    for item in ["a value", "another value"]
        yield item

2

생성기를 반환하는 함수를 정의 할 수 있습니다

def f():
  def FunctionWithYield(generator_args):
    code here...

  return FunctionWithYield

이제 원하는만큼 여러 번 할 수 있습니다.

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)

1
답변에 감사드립니다. 그러나 문제의 핵심은 창조 를 피하는 것이 었습니다. 내부 함수를 호출하면 창조가 숨겨집니다. 두 번 생성합니다
Dewfy

1

비싼 준비가 무슨 뜻인지 잘 모르겠지만 실제로는

data = ... # Expensive computation
y = FunctionWithYield(data)
for x in y: print(x)
#here must be something to reset 'y'
# this is expensive - data = ... # Expensive computation
# y = FunctionWithYield(data)
for x in y: print(x)

이 경우 재사용하지 data않겠습니까?


1

반복자를 재설정 할 수있는 옵션이 없습니다. 반복자는 일반적으로 next()기능을 반복 할 때 튀어 나옵니다 . 유일한 방법은 반복자 오브젝트에서 반복하기 전에 백업을 수행하는 것입니다. 아래를 확인하십시오.

0에서 9까지의 항목으로 반복자 객체 만들기

i=iter(range(10))

튀어 나오는 next () 함수를 반복

print(next(i))

반복자 객체를 목록으로 변환

L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

항목 0이 이미 튀어 나와 있습니다. 또한 반복자를 목록으로 변환하면 모든 항목이 팝업됩니다.

next(L) 

Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    next(L)
StopIteration

따라서 반복을 시작하기 전에 반복자를 백업 목록으로 변환해야합니다. 목록을 사용하여 반복자로 변환 할 수 있습니다iter(<list-object>)


1

이제 more_itertools.seekable반복자를 재설정 할 수있는 (타사 도구)를 사용할 수 있습니다.

통해 설치 > pip install more_itertools

import more_itertools as mit


y = mit.seekable(FunctionWithYield())
for x in y:
    print(x)

y.seek(0)                                              # reset iterator
for x in y:
    print(x)

참고 : 반복기를 진행하면서 메모리 소비가 증가하므로 큰 반복 가능 항목에주의하십시오.


1

당신은 사용하여 해당 할 수 itertools.cycle ()를 이 방법으로 반복자를 만든 다음 그 값을 통해 반복자 의지 루프를 통해 루프를 실행합니다.

예를 들면 다음과 같습니다.

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

0에서 4까지 20 개의 숫자를 반복해서 생성합니다.

문서의 메모 :

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

+1, 작동하지만 2 가지 문제가 있습니다. 1) 문서에 "사본 작성"으로 인한 메모리 사용량이 큽니다. 2) 무한 루프는 내가 원하는 것이 아닙니다.
Dewfy

0

좋아, 당신은 발전기를 여러 번 호출하고 싶다고 말하지만 초기화 비용이 비싸다.

class InitializedFunctionWithYield(object):
    def __init__(self):
        # do expensive initialization
        self.start = 5

    def __call__(self, *args, **kwargs):
        # do cheap iteration
        for i in xrange(5):
            yield self.start + i

y = InitializedFunctionWithYield()

for x in y():
    print x

for x in y():
    print x

또는 반복자 프로토콜을 따르고 일종의 '재설정'기능을 정의하는 자체 클래스를 만들 수도 있습니다.

class MyIterator(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self.i = 5

    def __iter__(self):
        return self

    def next(self):
        i = self.i
        if i > 0:
            self.i -= 1
            return i
        else:
            raise StopIteration()

my_iterator = MyIterator()

for x in my_iterator:
    print x

print 'resetting...'
my_iterator.reset()

for x in my_iterator:
    print x

https://docs.python.org/2/library/stdtypes.html#iterator-types http://anandology.com/python-practice-book/iterators.html


랩퍼에 문제를 위임하기 만하면됩니다. 값 비싼 초기화는 생성기를 생성한다고 가정합니다. 내 질문은 내부 재설정 방법에 관한 것입니다__call__
Dewfy

귀하의 의견에 대한 응답으로 두 번째 예를 추가했습니다. 이것은 본질적으로 리셋 방법을 갖춘 맞춤형 발전기입니다.
tvt173

0

내 대답은 약간 다른 문제를 해결합니다. 생성기가 초기화하는 데 비용이 많이 들고 생성 된 각 객체가 생성하는 데 비용이 많이 듭니다. 그러나 여러 기능에서 발전기를 여러 번 사용해야합니다. 생성기와 생성 된 각 객체를 정확히 한 번만 호출하기 위해 스레드를 사용하고 각 소비 메소드를 다른 스레드에서 실행할 수 있습니다. GIL로 인해 진정한 병렬 처리를 달성하지 못할 수도 있지만 목표를 달성 할 것입니다.

딥 러닝 모델은 많은 이미지를 처리합니다. 결과적으로 이미지의 많은 객체에 대한 많은 마스크가 만들어집니다. 각 마스크는 메모리를 소비합니다. 통계와 메트릭을 다르게 만드는 약 10 가지 방법이 있지만 모든 이미지를 한 번에 가져옵니다. 모든 이미지가 메모리에 맞지 않을 수 있습니다. 반복자를 수용하기 위해 모에 토트를 쉽게 다시 작성할 수 있습니다.

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

사용법 :

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

당신은 방금 재창조 itertools.islice하거나 async aiostream.stream.take를 위해이 게시물을 통해 asyn / await 방식으로 처리 할 수 ​​있습니다 stackoverflow.com/a/42379188/149818
Dewfy

-3

코드 객체로 수행 할 수 있습니다. 다음은 예입니다.

code_str="y=(a for a in [1,2,3,4])"
code1=compile(code_str,'<string>','single')
exec(code1)
for i in y: print i

12 34

for i in y: print i


exec(code1)
for i in y: print i

12 34


4
실제로 초기화 코드의 두 번 실행을 피하기 위해 실제로 발전기를 재설정해야했습니다. 귀하의 접근 방식 (1) 어쨌든 초기화를 두 번 실행합니다 (2) exec간단한 경우에는 약간 권장하지 않는 방법이 있습니다.
Dewfy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.