Python 생성기에서 한 요소 (peek)를 미리 보는 방법은 무엇입니까?


78

파이썬 생성기에서 한 요소를 미리 보는 방법을 알 수 없습니다. 내가 보자 마자 사라졌습니다.

내가 의미하는 바는 다음과 같습니다.

gen = iter([1,2,3])
next_value = gen.next()  # okay, I looked forward and see that next_value = 1
# but now:
list(gen)  # is [2, 3]  -- the first value is gone!

다음은 더 실제적인 예입니다.

gen = element_generator()
if gen.next_value() == 'STOP':
  quit_application()
else:
  process(gen.next())

하나의 요소를 앞으로 볼 수있는 생성기를 작성하도록 도와 줄 사람이 있습니까?


1
하고 싶은 일을 더 자세히 설명해 주시겠습니까? 코드 샘플일까요?
Tim Pietzcker 2010 년

기존 목록이있는 경우 무엇이 더 필요합니까? 또한 첫 번째 값을 next_value, no 로 저장하는 것 같습니다 .
SilentGhost 2010 년

SilentGhost, gone의미 를 설명하는 예였습니다 . 목록도없고 next_value도 없습니다. 생성기에서 요소가 사라지는 것이 의미하는 바를 보여주는 예일뿐입니다.
bodacydo 2010 년

@bodacydo : 아직도 이해가 안 돼요. 그럼 어떻게 됐어? 그 가치에 접근 할 수없는 이유는 무엇입니까?
SilentGhost 2010 년

Tim은 더 나은 예제로 질문을 업데이트했습니다.
bodacydo 2010 년

답변:


60

Python 생성기 API는 한 가지 방법입니다. 읽은 요소를 푸시 백 할 수 없습니다. 그러나 itertools 모듈을 사용하여 새 반복자를 만들고 요소 앞에 추가 할 수 있습니다 .

import itertools

gen = iter([1,2,3])
peek = gen.next()
print list(itertools.chain([peek], gen))

5
send이전에 산출 된 값을 다음 값을 산출 할 때 생성기로 다시 푸시하는 데 사용할 수 있습니다 .
dansalmo

2
@dansalmo : 예,하지만 생성기 코드를 수정해야합니다. Andrew Hare의 답변을 참조하십시오.
아론 Digulla

6
이 솔루션을 여러 번 사용했지만 기본적으로 itertools.chain.__next__ n반복 가능한 각 요소에 대해 시간을 호출한다는 점을 지적해야한다고 생각합니다 (피킹 n한 횟수는 어디 입니까). 이것은 하나 개 또는 두 개의 엿보기에 좋은 작품,하지만 당신은 모든 요소에서 들여다해야하는 경우,이 최선의 해결책 :-)하지 않습니다
mgilson

9
나는 이것이 more-itertools패키지에서 spy. 이 하나의 기능에 대해 완전히 새로운 패키지를 가져올 가치가 있다는 것은 말할 필요도 없지만 일부 사람들은 기존 구현이 유용하다고 생각할 수 있습니다.
David Z

@mgilson 예, 이것은 확실히 경고와 함께 와야합니다. 사람들은 각 요소를 들여다 보면서 루프에서 이것을 시도 할 수 있으며 전체 반복에는 2 차 시간이 걸립니다.
Kelly Bundy

80

완전성을 위해 more-itertools패키지 (아마도 Python 프로그래머 도구 상자의 일부 여야 함)에는 peekable이 동작을 구현 하는 래퍼가 포함되어 있습니다. 문서 의 코드 예제는 다음을 보여줍니다.

>>> p = peekable(['a', 'b'])
>>> p.peek()
'a'
>>> next(p)
'a'

그러나 실제로 필요하지 않도록이 기능을 사용하는 코드를 다시 작성할 수 있습니다. 예를 들어, 질문의 실제 코드 샘플은 다음과 같이 작성할 수 있습니다.

gen = element_generator()
command = gen.next_value()
if command == 'STOP':
  quit_application()
else:
  process(command)

(독자 주 : 구버전의 Python을 참조하더라도이 글을 작성할 때의 질문에서 예제의 구문을 보존했습니다.)


25

좋아-2 년이 늦었지만이 질문을 보았지만 만족스러운 답을 찾지 못했습니다. 이 메타 생성기로 나타났습니다.

class Peekorator(object):

    def __init__(self, generator):
        self.empty = False
        self.peek = None
        self.generator = generator
        try:
            self.peek = self.generator.next()
        except StopIteration:
            self.empty = True

    def __iter__(self):
        return self

    def next(self):
        """
        Return the self.peek element, or raise StopIteration
        if empty
        """
        if self.empty:
            raise StopIteration()
        to_return = self.peek
        try:
            self.peek = self.generator.next()
        except StopIteration:
            self.peek = None
            self.empty = True
        return to_return

def simple_iterator():
    for x in range(10):
        yield x*3

pkr = Peekorator(simple_iterator())
for i in pkr:
    print i, pkr.peek, pkr.empty

결과 :

0 3 False
3 6 False
6 9 False
9 12 False    
...
24 27 False
27 None False

즉, 반복하는 동안 언제든지 목록의 다음 항목에 액세스 할 수 있습니다.


1
나는 이것을 말하는 것을 약간 의미한다고 생각하지만이 솔루션은 끔찍하고 오류가 발생하기 쉽습니다. 언제든지 생성기에서 'i'및 'i + 1'요소의 두 항목에 액세스해야합니다. 다음 및 현재 값 대신 현재 및 이전 값을 사용하도록 알고리즘을 코딩하지 않는 이유는 무엇입니까? 완전히 동일하고 이것보다 훨씬 간단 해 보입니다.
조나단 하틀리

1
꼭-필요한만큼 비열
해지세요

6
@Jonathan 이것은 예를 들어 반복자가 함수에 전달 될 때와 같이 사소하지 않은 예에서는 항상 가능하지 않을 수 있습니다.
플로리안 레더 만

3
누군가는 python2.6 이후부터 생성기의 다음 값을 얻는 선호되는 방법 next(generator)generator.next(). IIRC generator.next()는 python3.x에서 사라집니다. 마찬가지로, 최상의 순방향 호환성 __next__ = next을 위해 python3.x에서 계속 작동하도록 클래스 본문에 추가하십시오 . 즉, 훌륭한 답변입니다.
mgilson

@mgilson을 에코하면 생성기가 문자열 반복기 인 경우 Python 3에서 작동하지 않습니다. 이를 위해 절대적으로 사용해야합니다next()
jpyams

16

itertools.tee를 사용하여 생성기의 경량 사본을 생성 할 수 있습니다. 그런 다음 한 사본을 미리 들여다 보면 두 번째 사본에는 영향을 미치지 않습니다.

import itertools

def process(seq):
    peeker, items = itertools.tee(seq)

    # initial peek ahead
    # so that peeker is one ahead of items
    if next(peeker) == 'STOP':
        return

    for item in items:

        # peek ahead
        if next(peeker) == "STOP":
            return

        # process items
        print(item)

'항목'생성기는 '피커'를 성추행해도 영향을받지 않습니다. '티'를 호출 한 후 원래 'seq'를 사용하면 안됩니다.

FWIW, 이것은 이 문제를 해결 하는 잘못된 방법입니다. 생성기에서 1 개 항목을 미리 봐야하는 알고리즘은 현재 생성기 항목과 이전 항목을 사용하도록 작성 될 수 있습니다. 그러면 생성기 사용을 망칠 필요가 없으며 코드가 훨씬 간단 해집니다. 이 질문에 대한 다른 답변을 참조하십시오.


3
"생성기에서 1 개 항목을 미리 확인해야하는 알고리즘은 현재 생성기 항목과 이전 항목을 사용하도록 작성 될 수 있습니다." 생성기 사용을 변경하면 특히 미리보기가 필요한 파서에서 더 우아하고 읽기 쉬운 코드로 이어질 수 있습니다.
Rufflewind

안녕, 러플 윈드. 나는 미리보기가 필요한 구문 분석에 대한 요점을 이해하지만 단순히 생성기에서 이전 항목을 저장하고 생성기에서 가장 최근 항목을 미리보기로 사용하여이를 달성 할 수없는 이유를 알 수 없습니다. 그런 다음 두 세계의 장점을 모두 얻을 수 있습니다. 즉, 얽 히지 않은 생성기와 간단한 파서입니다.
Jonathan Hartley

글쎄, 그것이 자동으로 이것을 수행하기 위해 생성기를 사용자 정의 클래스로 래핑하는 이유입니다.
Rufflewind

안녕 러펠 윈드. 나는 더 이상 당신이 옹호하는 것을 이해하는지 확신 할 수 없습니다. 줄거리를 잃어서 죄송합니다.
Jonathan Hartley

1
FWIW, 이제 코드가 수정되었습니다. @Eric \ May의 전체 반복기가 버퍼링된다는 의견은 더 이상 사실이 아닙니다.
Jonathan Hartley

5
>>> gen = iter(range(10))
>>> peek = next(gen)
>>> peek
0
>>> gen = (value for g in ([peek], gen) for value in g)
>>> list(gen)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

여기서 무슨 일이 일어나고 있는지
Kristof Pal

우리는 세대를 엿 봅니다. 그런 다음 반복 가능한 [peek]을 만들고 나머지 gen과 결합하여 새로운 gen을 만듭니다. 이것은 원본을 제공하기 위해 결합되는 두 생성기의 평탄화를 반복함으로써 수행됩니다. flatting 참조 : stackoverflow.com/questions/952914/…
로버트 킹

1
이것은 동일하지만 itertools.chain 솔루션보다 더 명시 적입니다.
Theo Belaire 2014

5

재미를 위해 Aaron의 제안에 따라 lookahead 클래스 구현을 만들었습니다.

import itertools

class lookahead_chain(object):
    def __init__(self, it):
        self._it = iter(it)

    def __iter__(self):
        return self

    def next(self):
        return next(self._it)

    def peek(self, default=None, _chain=itertools.chain):
        it = self._it
        try:
            v = self._it.next()
            self._it = _chain((v,), it)
            return v
        except StopIteration:
            return default

lookahead = lookahead_chain

이를 통해 다음이 작동합니다.

>>> t = lookahead(xrange(8))
>>> list(itertools.islice(t, 3))
[0, 1, 2]
>>> t.peek()
3
>>> list(itertools.islice(t, 3))
[3, 4, 5]

이 구현에서는 peek를 연속으로 여러 번 호출하는 것이 좋지 않습니다.

CPython 소스 코드를 살펴보면서 더 짧고 효율적인 방법을 찾았습니다.

class lookahead_tee(object):
    def __init__(self, it):
        self._it, = itertools.tee(it, 1)

    def __iter__(self):
        return self._it

    def peek(self, default=None):
        try:
            return self._it.__copy__().next()
        except StopIteration:
            return default

lookahead = lookahead_tee

사용법은 위와 동일하지만 연속해서 여러 번 peek를 사용하기 위해 여기에서 가격을 지불하지 않습니다. 몇 줄을 더 사용하면 반복기에서 둘 이상의 항목을 미리 볼 수도 있습니다 (사용 가능한 RAM까지).


4

항목 (i, i + 1)을 사용하는 대신, 여기서 'i'는 현재 항목이고 i + 1은 '앞서보기'버전 인 경우 (i-1, i)를 사용해야합니다. 여기서 'i-1' 생성기의 이전 버전입니다.

이런 방식으로 알고리즘을 조정하면 '앞서 살펴보기'를 시도하는 불필요한 복잡성을 제외하고 현재 보유하고있는 것과 동일한 것을 생성 할 수 있습니다.

앞을 들여다 보는 것은 실수이며 그렇게해서는 안됩니다.


원하는지 알기 전에 발전기에서 항목을 꺼내야합니다. 함수가 생성기에서 항목을 가져오고 검사시 원하지 않는다고 결정한다고 가정 해 보겠습니다. 생성기의 다음 사용자는 다시 밀어 넣을 수 없으면 해당 항목을 볼 수 없습니다. 엿보기는 항목을 뒤로 밀 필요를 제거합니다.
Isaac Turner

@IsaacTurner 아니요, 그렇게 할 필요가 없습니다. 예를 들어 두 개의 중첩 생성기가있을 수 있습니다. 내면의 사람은 항목을 가져 와서 아무것도하고 싶지 않다고 결정한 다음 상관없이 그것을 산출합니다. 바깥 쪽은 여전히 ​​순서대로 모든 것을 봅니다. 중첩 생성기없이 동일한 작업을 수행하는 매우 간단한 방법이 있습니다. 변수의 '이전 항목'을 기억하면이 질문에서 요청하는 모든 작업을 수행 할 수 있습니다. 일을 되 돌리는 것보다 훨씬 간단합니다.
Jonathan Hartley 2015

4

간단한 해결책은 다음과 같은 함수를 사용하는 것입니다.

def peek(it):
    first = next(it)
    return first, itertools.chain([first], it)

그런 다음 다음을 수행 할 수 있습니다.

>>> it = iter(range(10))
>>> x, it = peek(it)
>>> x
0
>>> next(it)
0
>>> next(it)
1

3

이것은 작동합니다-항목을 버퍼링하고 시퀀스의 각 항목과 다음 항목으로 함수를 호출합니다.

시퀀스가 끝날 때 발생하는 일에 대한 요구 사항이 모호합니다. 마지막에있을 때 "미리보기"는 무엇을 의미합니까?

def process_with_lookahead( iterable, aFunction ):
    prev= iterable.next()
    for item in iterable:
        aFunction( prev, item )
        prev= item
    aFunction( item, None )

def someLookaheadFunction( item, next_item ):
    print item, next_item

3

누구든지 관심이 있고 내가 틀렸다면 저를 정정하십시오. 그러나 어떤 반복자에 어떤 푸시 백 기능을 추가하는 것은 꽤 쉽다고 생각합니다.

class Back_pushable_iterator:
    """Class whose constructor takes an iterator as its only parameter, and
    returns an iterator that behaves in the same way, with added push back
    functionality.

    The idea is to be able to push back elements that need to be retrieved once
    more with the iterator semantics. This is particularly useful to implement
    LL(k) parsers that need k tokens of lookahead. Lookahead or push back is
    really a matter of perspective. The pushing back strategy allows a clean
    parser implementation based on recursive parser functions.

    The invoker of this class takes care of storing the elements that should be
    pushed back. A consequence of this is that any elements can be "pushed
    back", even elements that have never been retrieved from the iterator.
    The elements that are pushed back are then retrieved through the iterator
    interface in a LIFO-manner (as should logically be expected).

    This class works for any iterator but is especially meaningful for a
    generator iterator, which offers no obvious push back ability.

    In the LL(k) case mentioned above, the tokenizer can be implemented by a
    standard generator function (clean and simple), that is completed by this
    class for the needs of the actual parser.
    """
    def __init__(self, iterator):
        self.iterator = iterator
        self.pushed_back = []

    def __iter__(self):
        return self

    def __next__(self):
        if self.pushed_back:
            return self.pushed_back.pop()
        else:
            return next(self.iterator)

    def push_back(self, element):
        self.pushed_back.append(element)
it = Back_pushable_iterator(x for x in range(10))

x = next(it) # 0
print(x)
it.push_back(x)
x = next(it) # 0
print(x)
x = next(it) # 1
print(x)
x = next(it) # 2
y = next(it) # 3
print(x)
print(y)
it.push_back(y)
it.push_back(x)
x = next(it) # 2
y = next(it) # 3
print(x)
print(y)

for x in it:
    print(x) # 4-9

1

@ jonathan-hartley 답변에 대한 Python3 스 니펫 :

def peek(iterator, eoi=None):
    iterator = iter(iterator)

    try:
        prev = next(iterator)
    except StopIteration:
        return iterator

    for elm in iterator:
        yield prev, elm
        prev = elm

    yield prev, eoi


for curr, nxt in peek(range(10)):
    print((curr, nxt))

# (0, 1)
# (1, 2)
# (2, 3)
# (3, 4)
# (4, 5)
# (5, 6)
# (6, 7)
# (7, 8)
# (8, 9)
# (9, None)

이 작업을 수행하고 항목 __iter__만 산출 하고 일부 속성을 prev입력 하는 클래스를 만드는 것은 간단 elm합니다.


1

@David Z의 게시물에 따르면 최신 seekable도구는 래핑 된 반복기를 이전 위치로 재설정 할 수 있습니다.

>>> s = mit.seekable(range(3))
>>> s.next()
# 0

>>> s.seek(0)                                              # reset iterator
>>> s.next()
# 0

>>> s.next()
# 1

>>> s.seek(1)
>>> s.next()
# 1

>>> next(s)
# 2

1

cytoolz 에는 엿보기 기능이 있습니다.

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]

1

다음 요소와 더 앞선 요소를 엿볼 수있는 반복기입니다. 필요에 따라 미리 읽고 deque.

from collections import deque

class PeekIterator:

    def __init__(self, iterable):
        self.iterator = iter(iterable)
        self.peeked = deque()

    def __iter__(self):
        return self

    def __next__(self):
        if self.peeked:
            return self.peeked.popleft()
        return next(self.iterator)

    def peek(self, ahead=0):
        while len(self.peeked) <= ahead:
            self.peeked.append(next(self.iterator))
        return self.peeked[ahead]

데모:

>>> it = PeekIterator(range(10))
>>> it.peek()
0
>>> it.peek(5)
5
>>> it.peek(13)
Traceback (most recent call last):
  File "<pyshell#68>", line 1, in <module>
    it.peek(13)
  File "[...]", line 15, in peek
    self.peeked.append(next(self.iterator))
StopIteration
>>> it.peek(2)
2
>>> next(it)
0
>>> it.peek(2)
3
>>> list(it)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

0

itertools.chain()이 작업을위한 자연스러운 도구 이지만 다음과 같은 루프에주의하십시오.

for elem in gen:
    ...
    peek = next(gen)
    gen = itertools.chain([peek], gen)

... 이는 선형 적으로 증가하는 메모리 양을 소비하고 결국 중단되기 때문입니다. (이 코드는 본질적으로 chain () 호출 당 하나의 노드로 연결된 목록을 생성하는 것처럼 보입니다.) libs를 검사했기 때문이 아니라 이로 인해 프로그램이 크게 느려지기 때문에이 gen = itertools.chain([peek], gen)줄 을 제거하면 속도가 빨라졌습니다. 다시. (Python 3.3)

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.