Python 3.x: проверка наличия в генераторе оставшихся элементов - PullRequest
15 голосов
/ 22 декабря 2011

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

Я знаю только: next () вызывает исключение (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 ]

20 голосов
/ 28 декабря 2011

Это отличный вопрос.Я постараюсь показать вам, как мы можем использовать интроспективные способности 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))
5 голосов
/ 22 декабря 2011

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

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

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

2 голосов
/ 23 февраля 2016

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

На практике возникает вопрос, когда кто-то хочет сейчас взять только один или несколько элементов из итератора, но не хочет писать этот ужасный код обработки исключений (как указано в вопросе). Действительно, непитонно вкладывать понятие "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))

Однако некоторые итераторы предоставляют подсказку « length » ( PEP424 ):

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

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

0 голосов
/ 16 мая 2018

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

def g():
    yield 5

result = next(g(), None)

Теперь result - это либо 5, либо None, в зависимости от того, сколько раз вы вызывали следующий элемент на итераторе, или в зависимости от того, возвращала ли функция генератора раньше, чем результат.

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

...