Как посмотреть на один элемент в генераторе 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())
Может кто-нибудь помочь мне написать генератор, который вы можете посмотреть на один элемент вперед?
18 ответов
API генератора Python - это один из способов: вы не можете сдвинуть назад прочитанные элементы. Но вы можете создать новый итератор, используя модуль itertools и добавив элемент:
import itertools
gen = iter([1,2,3])
peek = gen.next()
print list(itertools.chain([peek], gen))
Для полноты more-itertools
пакет (который, вероятно, должен быть частью набора инструментов любого программиста Python) включает в себя 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 элемент вперед в генераторе, мог бы быть написан так, чтобы использовать текущий элемент генератора и предыдущий элемент. Тогда вам не придется ломать голову над использованием генераторов, и ваш код будет намного проще. Смотрите мой другой ответ на этот вопрос.
Итератор, который позволяет заглядывать в следующий элемент, а также вперед. Он читает вперед по мере необходимости и запоминает значения в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]
>>>
Простое решение - использовать функцию, подобную этой:
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
>>> 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]
Просто для забавы я создал реализацию преднамеренного класса на основе предложения Аарона:
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 много раз подряд. Имея еще несколько строк, вы также можете просмотреть несколько элементов в итераторе (вплоть до доступной оперативной памяти).
Если кому-то интересно, и, пожалуйста, поправьте меня, если я ошибаюсь, но я считаю, что довольно легко добавить некоторую функциональность 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
Это будет работать - он буферизует элемент и вызывает функцию для каждого элемента и следующего элемента в последовательности.
Ваши требования неясны в отношении того, что происходит в конце последовательности. Что значит "смотреть вперед", когда вы на последнем?
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
Вместо использования элементов (i, i+1), где "i" - текущий элемент, а i + 1 - версия "заглянуть вперед", следует использовать (i-1, i), где "i-1" это предыдущая версия от генератора.
Изменение вашего алгоритма таким образом приведет к тому, что у вас будет то же самое, что и у вас, за исключением дополнительной ненужной сложности попыток "заглянуть вперед".
Заглядывать вперёд - это ошибка, и вы не должны этого делать.
Алгоритм, который работает, «заглядывая» в следующий элемент в генераторе, может быть эквивалентно алгоритму, который работает, запоминая предыдущий элемент , рассматривая этот элемент как тот, над которым нужно работать, и рассматривая «текущий» элемент как просто «просматриваемый». .
В любом случае, на самом деле происходит то, что алгоритм учитывает перекрывающиеся пары из генератора. itertools.tee
recipe будет работать нормально — и нетрудно заметить, что это, по сути, переработанная версия подхода Джонатана Хартли :
from itertools import tee
# From https://docs.python.org/3/library/itertools.html#itertools.pairwise
# In 3.10 and up, this is directly supplied by the `itertools` module.
def pairwise(iterable):
# pairwise('ABCDEFG') --> AB BC CD DE EF FG
a, b = tee(iterable)
next(b, None)
return zip(a, b)
def process(seq):
for to_process, lookahead in pairwise(seq):
# peek ahead
if lookahead == "STOP":
return
# process items
print(to_process)
Фрагмент 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
в каком-то атрибуте.
В моем случае мне нужен генератор, в котором я мог бы вернуться в очередь, чтобы сгенерировать данные, которые я только что получил с помощью вызова next ().
Я решаю эту проблему, создавая очередь. В реализации генератора я бы сначала проверил очередь: если очередь не пуста, «yield» вернет значения в очереди, или, в противном случае, значения будут обычным образом.
import queue
def gen1(n, q):
i = 0
while True:
if not q.empty():
yield q.get()
else:
yield i
i = i + 1
if i >= n:
if not q.empty():
yield q.get()
break
q = queue.Queue()
f = gen1(2, q)
i = next(f)
print(i)
i = next(f)
print(i)
q.put(i) # put back the value I have just got for following 'next' call
i = next(f)
print(i)
Бег
python3 gen_test.py
0
1
1
Эта концепция очень полезна, когда я писал синтаксический анализатор, который должен просматривать файл построчно, и если строка кажется принадлежащей следующей фазе синтаксического анализа, я мог бы просто вернуться в очередь к генератору, чтобы следующая фаза кода могла правильно проанализировать его, не обрабатывая сложное состояние.
cytoolz имеет функцию просмотра.
>> from cytoolz import peek
>> gen = iter([1,2,3])
>> first, continuation = peek(gen)
>> first
1
>> list(continuation)
[1, 2, 3]
wrt @David Z post, новее 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
Хотя itertools.chain()
это естественный инструмент для работы здесь, остерегайтесь таких петель:
for elem in gen:
...
peek = next(gen)
gen = itertools.chain([peek], gen)
... Потому что это будет потреблять линейно растущий объем памяти и в конечном итоге остановится. (Похоже, этот код создает связанный список, по одному узлу на вызов chain().) Я знаю это не потому, что проверял библиотеки, а потому, что это привело к значительному замедлению моей программы - избавлению от gen = itertools.chain([peek], gen)
линия снова ускорилась. (Python 3.3)
Для тех из вас, кто любит бережливость и однострочность, я представляю вам однострочник, который позволяет смотреть вперед в итеративном режиме (это работает только в Python 3.8 и выше):
>>> import itertools as it
>>> peek = lambda iterable, n=1: it.islice(zip(it.chain((t := it.tee(iterable))[0], [None] * n), it.chain([None] * n, t[1])), n, None)
>>> for lookahead, element in peek(range(10)):
... print(lookahead, element)
1 0
2 1
3 2
4 3
5 4
6 5
7 6
8 7
9 8
None 9
>>> for lookahead, element in peek(range(10), 2):
... print(lookahead, element)
2 0
3 1
4 2
5 3
6 4
7 5
8 6
9 7
None 8
None 9
Этот метод занимает мало места, поскольку позволяет избежать многократного копирования итератора. Это также быстро из-за того, что он лениво генерирует элементы. Наконец, как вишенка на вершине, вы можете смотреть вперед произвольное количество элементов.