В Python 3.x: проверить, если генератор имеет оставшиеся элементы

когда я использую генератор в цикле for, он, кажется," знает", когда больше нет элементов. Теперь я должен использовать генератор без цикла for и использовать далее() вручную, чтобы получить следующий элемент. Моя проблема в том, как я узнаю, если больше нет элементов?

Я знаю только: далее() вызывает исключение (StopIteration), если ничего не осталось, но не является ли исключение слишком "тяжелым" для такой простой проблемы? Разве нет метод, как has_next() или так?

следующие строки должны прояснить, что я имею в виду:

#!/usr/bin/python3

# define a list of some objects
bar = ['abc', 123, None, True, 456.789]

# our primitive generator
def foo(bar):
    for b in bar:
        yield b

# iterate, using the generator above
print('--- TEST A (for loop) ---')
for baz in foo(bar):
    print(baz)
print()

# assign a new iterator to a variable
foobar = foo(bar)

print('--- TEST B (try-except) ---')
while True:
    try:
        print(foobar.__next__())
    except StopIteration:
        break
print()

# assign a new iterator to a variable
foobar = foo(bar)

# display generator members
print('--- GENERATOR MEMBERS ---')
print(', '.join(dir(foobar)))

вывод выглядит следующим образом:

--- TEST A (for loop) ---
abc
123
None
True
456.789

--- TEST B (try-except) ---
abc
123
None
True
456.789

--- GENERATOR MEMBERS ---
__class__, __delattr__, __doc__, __eq__, __format__, __ge__, __getattribute__, __gt__, __hash__, __init__, __iter__, __le__, __lt__, __name__, __ne__, __new__, __next__, __reduce__, __reduce_ex__, __repr__, __setattr__, __sizeof__, __str__, __subclasshook__, close, gi_code, gi_frame, gi_running, send, throw

спасибо всем, и хорошего дня! :)

4 ответов


два заявления, которые вы написали, имеют дело с поиском конца генератора точно таким же образом. For-loop просто вызывает .next () до тех пор, пока не будет вызвано исключение StopIteration, а затем оно завершится.

http://docs.python.org/tutorial/classes.html#iterators

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


это большой вопрос. Я попытаюсь показать вам, как мы можем использовать интроспективные способности Python и открытый исходный код, чтобы получить ответ. Мы можем использовать dis модуль, чтобы заглянуть за занавес и посмотреть, как интерпретатор CPython реализует цикл for над итератором.

>>> def for_loop(iterable):
...     for item in iterable:
...         pass  # do nothing
...     
>>> import dis
>>> dis.dis(for_loop)
  2           0 SETUP_LOOP              14 (to 17) 
              3 LOAD_FAST                0 (iterable) 
              6 GET_ITER             
        >>    7 FOR_ITER                 6 (to 16) 
             10 STORE_FAST               1 (item) 

  3          13 JUMP_ABSOLUTE            7 
        >>   16 POP_BLOCK            
        >>   17 LOAD_CONST               0 (None) 
             20 RETURN_VALUE         

сочный бит представляется FOR_ITER опкод. Мы не можем нырять глубже, используя dis, поэтому давайте посмотрим FOR_ITER в исходном коде интерпретатора CPython. Если вы осмотритесь, вы найдете его в Python/ceval.c; вы можете посмотреть это здесь. Вот в чем дело:

    TARGET(FOR_ITER)
        /* before: [iter]; after: [iter, iter()] *or* [] */
        v = TOP();
        x = (*v->ob_type->tp_iternext)(v);
        if (x != NULL) {
            PUSH(x);
            PREDICT(STORE_FAST);
            PREDICT(UNPACK_SEQUENCE);
            DISPATCH();
        }
        if (PyErr_Occurred()) {
            if (!PyErr_ExceptionMatches(
                            PyExc_StopIteration))
                break;
            PyErr_Clear();
        }
        /* iterator ended normally */
        x = v = POP();
        Py_DECREF(v);
        JUMPBY(oparg);
        DISPATCH();

вы видите, как это работает? Мы пытаемся захватить элемент из итератора; если мы терпим неудачу, мы проверяем, какое исключение было вызвано. Если это StopIteration, мы очищаем его и считаем, что итератор исчерпан.

Итак, как цикл for "просто знает", когда итератор был исчерпан? Ответ:нет - он должен попытаться захватить элемент. Но почему?

часть ответа-простота. Часть красота реализации итераторов заключается в том, что вам нужно определить только одну операцию: захватить следующий элемент. Но что более важно, это делает итераторы лень: они будут производить только те ценности, которые они совершенно.

наконец, если вам действительно не хватает этой функции, тривиально реализовать ее самостоятельно. Вот пример:

class LookaheadIterator:

    def __init__(self, iterable):
        self.iterator = iter(iterable)
        self.buffer = []

    def __iter__(self):
        return self

    def __next__(self):
        if self.buffer:
            return self.buffer.pop()
        else:
            return next(self.iterator)

    def has_next(self):
        if self.buffer:
            return True

        try:
            self.buffer = [next(self.iterator)]
        except StopIteration:
            return False
        else:
            return True


x  = LookaheadIterator(range(2))

print(x.has_next())
print(next(x))
print(x.has_next())
print(next(x))
print(x.has_next())
print(next(x))

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

на практике вопрос возникает, когда кто-то хочет взять только один или несколько элементов из итератора на данный момент, но не хочет писать этот уродливый код обработки исключений (как указано в вопросе). На самом деле это не обновления поставить концепция"StopIteration" в обычный код приложения. И обработка исключений на уровне python занимает довольно много времени, особенно когда речь идет только об одном элементе.

питонический способ справиться с этими ситуациями лучше всего использовать for .. break [.. else] как:

for x in iterator:
    do_something(x)
    break
else:
    it_was_exhausted()

или с помощью встроенного next() функция по умолчанию, как

x = next(iterator, default_value)

или с помощью помощников итератора, например, из itertools модуль для перемонтирования таких вещей, как:

max_3_elements = list(itertools.islice(iterator, 3))

некоторые однако итераторы выставляют "длина намек" (PEP424):

>>> gen = iter(range(3))
>>> gen.__length_hint__()
3
>>> next(gen)
0
>>> gen.__length_hint__()
2

Примечание: iterator.__next__() не должен использоваться обычным кодом приложения. Вот почему они переименовали его из iterator.next() в Вместо python2. И используя next() без дефолта не намного лучше ...


это не может точно ответить на ваш вопрос, но я нашел свой путь здесь Хотите элегантно захватить результат от генератора без необходимости писать try: блок. Немного погуглив позже я понял это:

def g():
    yield 5

result = next(g(), None)

теперь result либо 5 или None, в зависимости от того, сколько раз вы вызывали next на итераторе, или в зависимости от того, возвращалась ли функция генератора раньше, а не уступала.

я сильно предпочитаю обработку None как выход над повышением для" нормальных " условий, поэтому уклонение от try / catch здесь-большая победа. Если ситуация требует этого, есть также простое место для добавления значения по умолчанию, отличного от None.