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


78

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

Хто-небудь може допомогти мені написати генератор, що ви можете дивитись на один елемент вперед?


1
Чи можете ви описати більш докладно, що ви хочете зробити? Зразок коду, можливо?
Тім Пітцкер,

якщо у вас є існуючий список, що ще вам потрібно? Крім того, здається, ви зберігаєте перше значення як next_value, ні?
SilentGhost

SilentGhost, це був приклад, щоб проілюструвати, що goneозначає. У мене немає списку і у мене немає значення next_value. Це був лише приклад, щоб показати, що означає зникнення елемента з генератора.
bodacydo

@bodacydo: Я досі не розумію. Як це тоді зникло? Чому у вас немає доступу до цього значення?
SilentGhost,

Тім, оновив запитання на кращому прикладі.
bodacydo

Відповіді:


60

API генератора Python - це один із способів: ви не можете відштовхувати прочитані елементи. Але ви можете створити новий ітератор за допомогою модуля itertools і додати елемент до елемента:

import itertools

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

5
Ви можете використовувати sendдля повернення раніше отриманого значення назад у генератор, оскільки воно дає наступне значення.
dansalmo

2
@dansalmo: Так, але вам потрібно змінити код генератора для цього. Дивіться відповідь Ендрю Зайця.
Аарон Дігулла

6
Я використовував це рішення багато разів, але я думаю, що, мабуть, слід зазначити, що ви в основному викликаєте itertools.chain.__next__ nчас для кожного елемента, який ви отримуєте з ітерабельного (де nскільки разів ви заглянули). Це чудово підходить для одного чи двох заглядів, але якщо вам потрібно зазирнути до кожного елемента, це не найкраще рішення :-)
mgilson

9
Я б зазначив, що це реалізовано в more-itertoolsпакеті як spy. Не кажучи вже про те, що варто вводити цілком новий пакет лише для цієї єдиної функції, але деяким людям може знадобитися існуюча реалізація.
David Z

@mgilson Так, це неодмінно повинно мати попередження. Люди цілком можуть спробувати зробити це в циклі, заглядаючи в кожен елемент, і тоді вся ітерація займає квадратичний час.
Kelly Bundy

79

Для повноти 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

Гаразд - два роки запізно - але я зіткнувся з цим питанням і не знайшов жодної відповіді на своє задоволення. Придумав цей метагенератор:

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“. Чому б не кодувати свій алгоритм, щоб використовувати поточне та попереднє значення, замість наступного та поточного значення? Це здається абсолютно ідентичним і набагато простішим, ніж це.
Джонатан Хартлі,


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" після виклику "tee" на ньому, що призведе до поломки.

FWIW, це неправильний спосіб вирішення цієї проблеми. Будь-який алгоритм, який вимагає, щоб ви дивились на 1 елемент вперед у генераторі, можна альтернативно записати для використання поточного елемента генератора та попереднього елемента. Тоді вам не доведеться перекручувати використання генераторів, і ваш код буде набагато простішим. Дивіться мою іншу відповідь на це запитання.


3
"Будь-який алгоритм, який вимагає, щоб ви дивилися на 1 елемент вперед у генераторі, можна альтернативно записати для використання поточного елемента генератора та попереднього елемента." Керування використанням генераторів іноді може призвести до отримання більш елегантного та читабельного коду, особливо в парсерах, які потребують пошуку.
Rufflewind

Ей там, Раффлівінд. Я розумію суть про синтаксичний аналіз, який вимагає пошуку lookahead, але я не розумію, чому ви не можете цього досягти, просто зберігаючи попередній елемент з вашого генератора, а використовуючи найновіший елемент з вашого генератора як lookahead. Тоді ви отримаєте найкраще з обох світів: нерозбірний генератор та простий парсер.
Джонатан Хартлі

Ну, ось чому ви обертаєте генератор у спеціальний клас, щоб автоматично робити це.
Rufflewind

Гей, Руфелвінд. Я вже не впевнений, що розумію, за що ти захищаєшся. Вибачте, що втратив змову.
Джонатан Хартлі,

1
FWIW, код тепер виправлений, коментар @Eric \ May про те, що весь ітератор буферизований, вже не відповідає дійсності.
Джонатан Хартлі,

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]

ти не заперечуєш пояснити, що тут відбувається
Крістоф Пал

Ми відглядаємо ген. Потім ми створюємо ітерабельний [peek] і поєднуємо його з рештою gen для створення нового gen. Це робиться шляхом ітерації через згладжування двох генераторів, які поєднуються, щоб отримати оригінал. Див. Плющення: stackoverflow.com/questions/952914/…
Роберт Кінг

1
Це те саме, але більш чітке, ніж рішення itertools.chain.
Тео Белер,

5

Для розваги я створив реалізацію класу 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

Використання таке саме, як і вище, але тут ви не заплатите ціну, щоб використовувати підглядання багато разів поспіль. Маючи ще кілька рядків, ви також можете заглянути вперед більше одного елемента в ітераторі (до доступної оперативної пам'яті).


4

Замість того, щоб використовувати елементи (i, i + 1), де 'i' є поточним елементом, а i + 1 - версія 'peek forward', вам слід використовувати (i-1, i), де 'i-1' є попередньою версією генератора.

Таким чином, налаштувавши свій алгоритм, ви отримаєте щось ідентичне тому, що ви маєте на даний час, крім зайвої зайвої складності спроб заглянути вперед.

Заглядати вперед - це помилка, і ви не повинні цього робити.


Вам потрібно вийняти предмет з генератора, перш ніж зрозуміти, чи хочете ви його. Скажімо, функція бере предмет із генератора, а після перевірки вирішує, що не хоче його. Наступний користувач генератора не побачить цей елемент, якщо ви не зможете відсунути його назад. Заглядаючи видаляє, потрібно відсунути предмети назад.
Ісаак Тернер

@IsaacTurner Ні, вам не потрібно цього робити. Наприклад, ви можете мати два вкладені генератори. Внутрішній бере предмет, вирішує, що не хоче з ним нічого робити, потім віддає його незалежно. Зовнішній все ще бачить все в послідовності. Існують рівнозначні, дуже прості способи зробити те саме без вкладених генераторів. Просто запам'ятайте "попередній елемент" у змінній, і ви зможете зробити все, що вимагає це питання. НАБАГАТО простіше, ніж спроби відсунути речі назад.
Джонатан Хартлі,

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

Фрагмент 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атрибут.


1

wrt @David Z's 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


1

Ітератор, який дозволяє зазирнути до наступного елемента, а також далі вперед. Він читає вперед, коли це потрібно, і запам'ятовує значення в a 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)

... Тому що це споживатиме лінійно зростаючу кількість пам’яті і врешті зупиниться. (Цей код, по суті, створює зв’язаний список, по одному вузлу на виклик ланцюга ().) Я знаю це не тому, що перевірив бібліотеки, а тому, що це просто призвело до значного уповільнення моєї програми - позбавлення від gen = itertools.chain([peek], gen)рядка пришвидшило знову. (Python 3.3)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.