Использование lookahead с генераторами
Я реализовал на Python сканер с генератором, который токенизирует строку в кортежей формы (тип токена, значение токена):
for token in scan("a(b)"):
print token
будет печатать
("literal", "a")
("l_paren", "(")
...
Следующая задача подразумевает анализ потока токенов, и для этого мне нужно иметь возможность искать один элемент вперед от текущего, не перемещая указатель вперед. Тот факт, что итераторы и генераторы не обеспечивают полную последовательность элементов одновременно, но каждый элемент по мере необходимости делает взгляды немного более сложными по сравнению с списками, поскольку следующий элемент не известен, если не вызывается __next__()
.
Что может показаться простой реализацией основанного на генераторе lookahead? В настоящее время я использую обходное решение, которое подразумевает создание списка из генератора:
token_list = [token for token in scan(string)]
Теперь вид легко реализуется примерно так:
try:
next_token = token_list[index + 1]
except: IndexError:
next_token = None
Конечно, это просто отлично работает. Но, думая, что мой второй вопрос возникает: действительно ли возникает вопрос создания генератора scan()
в первую очередь?
Ответы
Ответ 1
Вы можете написать оболочку, которая буферизует некоторое количество элементов из генератора и предоставляет функцию lookahead() для просмотра этих буферизированных элементов:
class Lookahead:
def __init__(self, iter):
self.iter = iter
self.buffer = []
def __iter__(self):
return self
def next(self):
if self.buffer:
return self.buffer.pop(0)
else:
return self.iter.next()
def lookahead(self, n):
"""Return an item n entries ahead in the iteration."""
while n >= len(self.buffer):
try:
self.buffer.append(self.iter.next())
except StopIteration:
return None
return self.buffer[n]
Ответ 2
Довольно хорошие ответы там, но мой любимый подход состоял бы в использовании itertools.tee
- с учетом итератора, он возвращает два (или более, если требуется), которые могут быть созданы независимо. Он буферизует в памяти столько, сколько необходимо (т.е. Не так много, если итераторы не получают очень "неуправляемого" друг от друга). Например:.
import itertools
import collections
class IteratorWithLookahead(collections.Iterator):
def __init__(self, it):
self.it, self.nextit = itertools.tee(iter(it))
self._advance()
def _advance(self):
self.lookahead = next(self.nextit, None)
def __next__(self):
self._advance()
return next(self.it)
Вы можете обернуть любой итератор этим классом, а затем использовать атрибут .lookahead
обертки, чтобы узнать, каким будет следующий элемент, который будет возвращен в будущем. Мне нравится оставлять всю настоящую логику на itertools.tee и просто предоставить этот тонкий клей! -)
Ответ 3
Это не очень, но это может сделать то, что вы хотите:
def paired_iter(it):
token = it.next()
for lookahead in it:
yield (token, lookahead)
token = lookahead
yield (token, None)
def scan(s):
for c in s:
yield c
for this_token, next_token in paired_iter(scan("ABCDEF")):
print "this:%s next:%s" % (this_token, next_token)
Печать
this:A next:B
this:B next:C
this:C next:D
this:D next:E
this:E next:F
this:F next:None
Ответ 4
Вот пример, который позволяет отправить один элемент в генератор
def gen():
for i in range(100):
v=yield i # when you call next(), v will be set to None
if v:
yield None # this yields None to send() call
v=yield v # so this yield is for the first next() after send()
g=gen()
x=g.next()
print 0,x
x=g.next()
print 1,x
x=g.next()
print 2,x # oops push it back
x=g.send(x)
x=g.next()
print 3,x # x should be 2 again
x=g.next()
print 4,x
Ответ 5
Построить простую оболочку с помощью itertools.tee:
from itertools import tee, islice
class LookAhead:
'Wrap an iterator with lookahead indexing'
def __init__(self, iterator):
self.t = tee(iterator, 1)[0]
def __iter__(self):
return self
def next(self):
return next(self.t)
def __getitem__(self, i):
for value in islice(self.t.__copy__(), i, None):
return value
raise IndexError(i)
Используйте класс для переноса существующего итератора или итератора. Затем вы можете либо итерации, обычно используя следующий, либо вы можете смотреть с индексированным поиском.
>>> it = LookAhead([10, 20, 30, 40, 50])
>>> next(it)
10
>>> it[0]
20
>>> next(it)
20
>>> it[0]
30
>>> list(it)
[30, 40, 50]
Чтобы запустить этот код под Python 3, просто измените следующий метод на __next __.
Ответ 6
Так как вы говорите, что вы токенизируете строку, а не общую итерабельную, я предлагаю простейшее решение просто расширить ваш токенизатор, чтобы вернуть 3-кортеж:
(token_type, token_value, token_index)
, где token_index
- индекс токена в строке. Затем вы можете смотреть вперед, назад или где-либо еще в строке. Просто не проходите мимо конца. Я думаю, что самое простое и гибкое решение.
Кроме того, вам не нужно использовать понимание списка для создания списка из генератора. Просто вызовите конструктор list() на нем:
token_list = list(scan(string))
Ответ 7
Пол - хороший ответ. Подход на основе класса с произвольным просмотром может выглядеть примерно так:
class lookahead(object):
def __init__(self, generator, lookahead_count=1):
self.gen = iter(generator)
self.look_count = lookahead_count
def __iter__(self):
self.lookahead = []
self.stopped = False
try:
for i in range(self.look_count):
self.lookahead.append(self.gen.next())
except StopIteration:
self.stopped = True
return self
def next(self):
if not self.stopped:
try:
self.lookahead.append(self.gen.next())
except StopIteration:
self.stopped = True
if self.lookahead != []:
return self.lookahead.pop(0)
else:
raise StopIteration
x = lookahead("abcdef", 3)
for i in x:
print i, x.lookahead
Ответ 8
Как я написал бы это кратко, если бы мне просто понадобилось 1 элемент оценки:
SEQUENCE_END = object()
def lookahead(iterable):
iter = iter(iterable)
current = next(iter)
for ahead in iter:
yield current,ahead
current = ahead
yield current,SEQUENCE_END
Пример:
>>> for x,ahead in lookahead(range(3)):
>>> print(x,ahead)
0, 1
1, 2
2, <object SEQUENCE_END>