Как заглянуть вперед на один элемент (peek) в генераторе Python?

StackOverflow https://stackoverflow.com/questions/2425270

  •  19-09-2019
  •  | 
  •  

Вопрос

Я не могу понять, как предвидеть один элемент в генераторе Python.Как только я смотрю, все исчезает.

Вот что я имею в виду:

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

Кто-нибудь может помочь мне написать генератор, который позволяет заглядывать на один элемент вперед?

Это было полезно?

Решение

API Python Generator API - это один из способов: вы не можете оттолкнуть элементы, которые прочитали. Но вы можете создать новый итератор, используя модуль итул и подготовить элемент:

import itertools

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

Другие советы

Ради полноты, more-itertools упаковка (что, вероятно, должно быть частью набора инструментов для любого программиста) включает в себя peekable обертка, которая реализует это поведение. Как пример кода в документация шоу:

>>> p = peekable(xrange(2))
>>> p.peek()
0
>>> p.next()
0
>>> p.peek()
1
>>> p.next()
1

Пакет совместим как с Python 2, так и с 3, хотя документация показывает синтаксис Python 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

т.е. у вас есть в любой момент во время итерации доступа к следующему элементу в списке.

Вы можете использовать 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» после вызова «TEE», который сломает вещи.

Fwiw, это неправильный Способ решить эту проблему. Любой алгоритм, который требует, чтобы вы смотрели на 1 элемент впереди в генераторе, можно альтернативно записано, чтобы использовать текущий элемент генератора и предыдущий элемент. Тогда вам не нужно использовать использование генераторов, и ваш код будет намного проще. Смотрите мой другой ответ на этот вопрос.

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

Просто для удовольствия я создал внедрение класса Lookahead на основе предложения Aaron:

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 много раз подряд. С еще несколькими линиями вы также можете посмотреть на более чем один элемент в итераторе (до доступной оперативной памяти).

Вместо использования элементов (i, i+1), где «я»-это текущий элемент, а i+1-версия «заглянуть вперед», вы должны использовать (i-1, i), где 'i-1' предыдущая версия из генератора.

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

Заглянуть вперед - ошибка, и вы не должны этого делать.

Это будет работать - он буферирует элемент и вызывает функцию с каждым элементом и следующим элементом в последовательности.

Ваши требования мрачны в том, что происходит в конце последовательности. Что означает «смотреть в будущее», когда вы находитесь на последнем?

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

Простое решение - использовать функцию, подобную этой:

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

Если кто -то заинтересован, и, пожалуйста, поправьте меня, если я ошибаюсь, но я считаю, что довольно легко добавить некоторые функциональные возможности для любого итератора.

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

Несмотря на то что itertools.chain() Это естественный инструмент для работы здесь, остерегайтесь таких петлей, как это:

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

... потому что это будет потреблять линейно растущее количество памяти и в конечном итоге остановиться. (Этот код, по сути, кажется, создает связанный список, один узел на цепный ().) gen = itertools.chain([peek], gen) Линия снова ускорила. (Python 3.3)

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 в некотором атрибуте.

wrt @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

Cytoolz имеет заглядывать функция

>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top