[python] 압축 된 2 세대 파이썬 생성기 : 조용히 소비되는 요소를 검색하는 방법

다음과 같이 (잠재적으로) 길이가 다른 2 개의 발전기를 구문 분석하고 싶습니다 zip.

for el1, el2 in zip(gen1, gen2):
    print(el1, el2)

그러나 gen2요소가 적 으면 추가 요소 중 하나 gen1가 “소비”됩니다.

예를 들어

def my_gen(n:int):
    for i in range(n):
        yield i

gen1 = my_gen(10)
gen2 = my_gen(8)

list(zip(gen1, gen2))  # Last tuple is (7, 7)
print(next(gen1))  # printed value is "9" => 8 is missing

gen1 = my_gen(8)
gen2 = my_gen(10)

list(zip(gen1, gen2))  # Last tuple is (7, 7)
print(next(gen2))  # printed value is "8" => OK

분명히, 더 이상 요소가 없다는 것을 깨닫기 전에 읽히므로 (따라서 값을 생성 8하기 때문에) 값이 누락되었습니다 ( 이전 예제에서) 그러나이 값은 우주에서 사라집니다. 때 “이상”입니다, 그런 “문제”가 없다.gen18gen2gen2

질문 :이 누락 된 값을 검색하는 방법이 8있습니까 (예 : 이전 예에서)? … 다양한 수의 인수를 사용하는 것이 이상적입니다 zip.

참고 : 현재 다른 방법으로 사용하여 구현 itertools.zip_longest했지만이 누락 값을 사용 zip하거나 이와 동등한 방법을 궁금하게 생각합니다 .

참고 2 : 새로운 구현을 제출하고 시도하려는 경우이 REPL에서 다양한 구현에 대한 테스트를 만들었습니다. : https://repl.it/@jfthuong/MadPhysicistChester



답변

한 가지 방법은 마지막 값을 캐시 할 수있는 생성기를 구현하는 것입니다.

class cache_last(collections.abc.Iterator):
    """
    Wraps an iterable in an iterator that can retrieve the last value.

    .. attribute:: obj

       A reference to the wrapped iterable. Provided for convenience
       of one-line initializations.
    """
    def __init__(self, iterable):
        self.obj = iterable
        self._iter = iter(iterable)
        self._sentinel = object()

    @property
    def last(self):
        """
        The last object yielded by the wrapped iterator.

        Uninitialized iterators raise a `ValueError`. Exhausted
        iterators raise a `StopIteration`.
        """
        if self.exhausted:
            raise StopIteration
        return self._last

    @property
    def exhausted(self):
        """
        `True` if there are no more elements in the iterator.
        Violates EAFP, but convenient way to check if `last` is valid.
        Raise a `ValueError` if the iterator is not yet started.
        """
        if not hasattr(self, '_last'):
            raise ValueError('Not started!')
        return self._last is self._sentinel

    def __next__(self):
        """
        Retrieve, record, and return the next value of the iteration.
        """
        try:
            self._last = next(self._iter)
        except StopIteration:
            self._last = self._sentinel
            raise
        # An alternative that has fewer lines of code, but checks
        # for the return value one extra time, and loses the underlying
        # StopIteration:
        #self._last = next(self._iter, self._sentinel)
        #if self._last is self._sentinel:
        #    raise StopIteration
        return self._last

    def __iter__(self):
        """
        This object is already an iterator.
        """
        return self

이를 사용하려면 입력을 zip다음으로 랩핑하십시오 .

gen1 = cache_last(range(10))
gen2 = iter(range(8))
list(zip(gen1, gen2))
print(gen1.last)
print(next(gen1)) 

gen2iterable 대신 iterator 를 만드는 것이 중요 하므로 어느 것이 소진되었는지 알 수 있습니다. gen2소진 된 경우 확인하지 않아도됩니다 gen1.last.

다른 접근 방식은 별도의 반복 가능한 항목 대신 가변 가능한 반복 가능한 시퀀스를 허용하도록 zip을 재정의하는 것입니다. 그러면 iterables를 “탐색 된”항목이 포함 된 체인 버전으로 바꿀 수 있습니다.

def myzip(iterables):
    iterators = [iter(it) for it in iterables]
    while True:
        items = []
        for it in iterators:
            try:
                items.append(next(it))
            except StopIteration:
                for i, peeked in enumerate(items):
                    iterables[i] = itertools.chain([peeked], iterators[i])
                return
            else:
                yield tuple(items)

gens = [range(10), range(8)]
list(myzip(gens))
print(next(gens[0]))

이 접근법은 여러 가지 이유로 문제가 있습니다. 원본 iterable을 잃을뿐만 아니라 원본 객체를 객체로 대체하여 가질 수있는 유용한 속성을 잃게됩니다 chain.


답변

이것은 문서에zip 주어진 구현과 동일합니다.

def zip(*iterables):
    # zip('ABCD', 'xy') --> Ax By
    sentinel = object()
    iterators = [iter(it) for it in iterables]
    while iterators:
        result = []
        for it in iterators:
            elem = next(it, sentinel)
            if elem is sentinel:
                return
            result.append(elem)
        yield tuple(result)

첫 번째 예에서 gen1 = my_gen(10)gen2 = my_gen(8). 두 발전기는 모두 7 번째 반복까지 소비됩니다. 이제 8 번째 반복 gen1호출 에서는 8 elem = next(it, sentinel)을 반환하지만 gen2호출 elem = next(it, sentinel)할 때 sentinel(이것이 gen2소진 되었기 때문에 ) if elem is sentinel반환되고 만족되고 함수는 return과 stop을 실행합니다. 이제 next(gen1)9를 반환합니다.

두 번째 예에서 gen1 = gen(8)gen2 = gen(10). 두 발전기는 모두 7 번째 반복까지 소비됩니다. 이제 8 번째 반복에서 gen1호출 elem = next(it, sentinel)하는 수익률 sentinel(때문에이 점에 gen1소진)과 if elem is sentinel만족과 기능이 실행 반환 및 정지. 이제 next(gen2)8을 반환합니다.

Mad Physicist ‘s answer 에서 영감을 얻은 이 Gen래퍼를 사용 하여 이에 대응할 수 있습니다.

편집 : Jean-Francois T가 지적한 사례를 처리합니다 .

이터레이터에서 값이 소비되면 이터레이터에서 영원히 사라지고 이터레이터가 이터레이터를 다시 이터레이터에 다시 추가 할 수있는 적절한 돌연변이 방법이 없습니다. 한 가지 해결 방법은 마지막으로 소비 된 값을 저장하는 것입니다.

class Gen:
    def __init__(self,iterable):
        self.d = iter(iterable)
        self.sentinal = object()
        self.prev = self.sentinal
    def __iter__(self):
        return self
    @property
    def last_val_consumed(self):
        if self.prev is None:
            raise StopIteration
        if self.prev == self.sentinal:
            raise ValueError('Nothing has been consumed')
        return self.prev
    def __next__(self):
        self.prev = next(self.d,None)
        if self.prev is None:
            raise StopIteration
        return self.prev

예 :

# When `gen1` is larger than `gen2`
gen1 = Gen(range(10))
gen2 = Gen(range(8))
list(zip(gen1,gen2))
# [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)]
gen1.last_val_consumed
# 8 #as it was the last values consumed
next(gen1)
# 9
gen1.last_val_consumed
# 9

# 2. When `gen1` or `gen2` is empty
gen1 = Gen(range(0))
gen2 = Gen(range(5))
list(zip(gen1,gen2))
gen1.last_val_consumed
# StopIteration error is raised
gen2.last_val_consumed
# ValueError is raised saying `ValueError: Nothing has been consumed`


답변

나는 당신 이이 답변을 이미 발견했으며 의견에 올라 왔지만 대답을 할 것이라고 생각했습니다. itertools.zip_longest()짧은 생성기의 빈 값을 다음으로 대체하는 을 사용하려고합니다 None.

import itertools

def my_gen(n:int):
    for i in range(n):
        yield i

gen1 = my_gen(10)
gen2 = my_gen(8)

for i, j in itertools.zip_longest(gen1, gen2):
    print(i, j)

인쇄물:

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 None
9 None

fillvalue호출 zip_longest할 때 인수를 None기본값 으로 대체 할 수도 있지만 기본적으로 for 루프에서 None( i또는 j)를 누르면 다른 변수에 8.


답변

@GrandPhuba의 설명에서 영감을 얻은 zip“안전한”변형 ( 여기서 단위 테스트 )을 만들어 보겠습니다 .

def safe_zip(*args):
    """
    Safe zip that restores last consumed element in eachgenerator
    if not able to consume an element in all of them

    Returns:
        * generators in tuple
        * generator for zipped generators
    """
  continue_ = True
  n = len(args)
  result = (_ for _ in [])
  while continue_:
    addend = []
    for i, gen in enumerate(args):
      try:
        value = next(gen)
        addend.append(value)
      except StopIteration:
        genlist = list(args)
        args = tuple([chain([v], g) for v, g in zip(addend, genlist[:i])]+genlist[i:])
        continue_ = False
        break
    if len(addend)==n: result = chain(result, [tuple(addend)])
  return args, result

기본 테스트는 다음과 같습니다.

    g1, g2 = (i for i in range(10)), (i for i in range(4))
    # Create (g1, g2), g3 first, then loop over g3 as one would with zip
    (g1, g2), g3 = safe_zip(g1, g2)
    for a, b in g3:
        print(a, b)#(0, 0) to (3, 3)
    for x in g1:
        print(x)#4 to 9


답변

itertools.teeitertools.islice를 사용할 수 있습니다 .

from itertools import islice, tee

def zipped(gen1, gen2, pred=list):
    g11, g12 = tee(gen1)
    z = pred(zip(g11, gen2))

    return (islice(g12, len(z), None), gen2), z

gen1 = iter(range(10))
gen2 = iter(range(5))

(gen1, gen2), output = zipped(gen1, gen2)

print(output)
print(next(gen1))
# [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]
# 5


답변

코드를 재사용하려면 가장 쉬운 해결책은 다음과 같습니다.

from more_itertools import peekable

a = peekable(a)
b = peekable(b)

while True:
    try:
        a.peek()
        b.peek()
    except StopIteration:
        break
    x = next(a)
    y = next(b)
    print(x, y)


print(list(a), list(b))  # Misses nothing.

설정을 사용하여이 코드를 테스트 할 수 있습니다.

def my_gen(n: int):
    yield from range(n)

a = my_gen(10)
b = my_gen(8)

인쇄됩니다 :

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


답변

소진 된 반복자 zip(..., ...).__iter__가 소진되면 삭제되고 액세스 할 수 없기 때문에 기본 for 루프를 사용하여 값을 검색 할 수 있다고 생각하지 않습니다 .

당신은 당신의 우편 번호를 변경해야합니다, 그리고 당신은 일부 해키 코드와 함께 드롭 항목의 위치를 ​​얻을 수 있습니다

z = zip(range(10), range(8))
for _ in iter(z.__next__, None):
    ...
_, (one, other) = z.__reduce__()
_, (i_one,), p_one = one.__reduce__() # p_one == current pos, 1 based
import itertools
val = next(itertools.islice(iter(i_one), p_one - 1, p_one))