Подтвердить что ты не робот

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

Я не могу понять, как искать один элемент в генераторе 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())

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

4b9b3361

Ответ 1

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

import itertools

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

Ответ 2

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

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

Пакет совместим с Python 2 и 3, хотя в документации показан синтаксис Python 2.

Ответ 3

Хорошо - два года слишком поздно, но я столкнулся с этим вопросом и не нашел ответа на мое удовлетворение. Пришел к этому метагенератору:

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

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

Ответ 4

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

Ответ 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]

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

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

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

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

Ответ 9

Простым решением является использование такой функции:

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

Ответ 10

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

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

Ответ 11

Хотя itertools.chain() является естественным инструментом для задания здесь, остерегайтесь таких циклов:

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

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

Ответ 12

Python3 для @jonathan-hartley ответ:

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

Ответ 13

wrt @David Z post, новый seekable инструмент может reset завернутый итератор в предыдущее положение.

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