В 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
.