Работая с генераторами через map, filter и all, я столкнулся с проблемой пустого массива:
проблема состоит в том, что, при передаче результата filter(...) в функцию all, а после при продолжении работы с генератором, полученным от функции filter, например, преобразуя его в tuple, чтобы взглянуть, какие элементы попали в массив после прохода через фильтр, я получал пустой tuple.
Абстрагируемся от всего, что нам не нужно, и рассмотрим саму проблему.
Пример 1.1
generator_reverse = (i for i in range(10, -1, -1))
print(all(generator_reverse))
print(tuple(generator_reverse))
input:
-> False
-> ()
Я специально перевернул элементы в range, чтобы было нагляднее.
Здесь all работает как нам и нужно, так как мы обходим generator_reverse впервые. Но дальше начинаются проблемы. tuple(generator_reverse) выдаёт пустой массив.
И вроде как... если задуматься, всё логично. В 4-ом часу ночи, истощенный, я так не думал :)
Генератор один раз прошёл по всем элементам в функции all и на последнем элементе, а именно на 0, завершил работу, вернув False, а при попытке второго обхода вследствие преобразования генератора в tuple сразу вызывается исключение StopIteration, и на выходе мы получаем пустой массив.
Конечно, если не писать четвёртую строку с преобразованием в tuple, вы этого даже не заметите, если, конечно, в будущем не собираетесь дальше работать с этим генератором.
Теперь я всё-таки верну массиву обычный шаг.
Пример 1.2
generator = (i for i in range(10))
print(all(generator))
print(tuple(generator))
input:
-> False
-> (1, 2, 3, 4, 5, 6, 7, 8, 9)
all 1 раз вызовет generator.next() и, получив 0, сразу вернёт False. generator сохранил состояние в ходе работы с all, и при преобразовании его в tuple мы получаем все элементы с того момента, как all закончил (при преобразовании мы потеряли нулевой элемент)
А что если мы преобразуем generator в tuple перед вызовом all?
Пример 1.3
generator = (i for i in range(10))
print(tuple(generator))
print(all(generator))
print(tuple(generator))
input:
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
True
()
При первом преобразовании всё хорошо. Мы обходим все элементы генератора и получаем ожидаемый массив. Но в all будет передан generator с состоянием на последнем элементе. В итоге сразу вызывается исключение StopIteration и all не увидев ни одного элемента в генераторе возвращает True, ну и в последующем преобразовании в tuple также сразу вызывается исключение StopIteration, возвращая, нам пустой массив.
(Просто показываю, что будет если передать пустой массив в функцию all)
bool_ = all(tuple())
print(bool_)
input:
True
Это натолкнуло меня на проверку поведения генератора, касающееся его динамичности.
Много кто знает, что если пополнять элементами массив, на основе которого построен генератор (уже после создания генератора), то они отобразятся и в генераторе, если, конечно, сам генератор до них дойдёт.
Пример 2.1
list_ = list()
gen = (i for i in list_)
list_.append(1)
list_.append(2)
print(next(gen))
list_.append(3)
list_.append(4)
list_.append(5)
print(tuple(gen))
input:
1
(2, 3, 4, 5)
Сначала мы создаём генератор на основе пустого списка. После добавляем в список 2 элемента. 1 раз вызываем next(gen). Всё как мы и ожидаем. Хоть мы добавили элементы в список уже после создания генератора.
Заметьте, на этом этапе мы не доходили до конца генератора и у нас не было вызвано исключение StopIteration.
После этого мы добавляем ещё несколько элементов в список и вызываем преобразование генератора в кортеж, тем самым получая все остальные элементы, включая те, что мы добавили уже после вызова next(gen). Всё работает как и ожидалось.
Но.. если перед добавлением элементов в массив уже дойти до конца генератора, вызвав StopIteration, то тут генератор умывает руки.
Пример 2.2
list_ = list()
gen = (i for i in list_)
list_.append(1)
list_.append(2)
print(tuple(gen))
list_.append(3)
list_.append(4)
list_.append(5)
print(tuple(gen))
input:
(1, 2)
()
Здесь происходит тоже самое, за исключением того, что мы всё таки вызываем StopIteration перед тем, как добавить все элементы в список. Те элементы, что мы успели добавить до исключения StopIteration, отразились в генераторе, но все остальные нет.
В дополнение к этому я спросил об этом нейронку:
Такое поведение можно объяснить тем, что генераторы являются итераторами, а это означает, что после того, как они достигли конца последовательности, по которой они перебираются, они не могут быть сброшены или перемотаны к началу. Таким образом, если генератор уже достиг конца своей последовательности, он больше не будет выдавать значения, даже если в базовую последовательность будут добавлены новые элементы.
Ответ ясен, и в целом на этом можно было бы закончить. Но я всё же попытался получить информацию, куда именно попадает ход выполнения если вызвать генератор после того, как он дошёл до конца.
Совершенно логично, что почти сразу вызывается исключение StopIteration. Но.. я хотел узнать где именно. К сожалению, следующий пример не дал мне никакой информации
Пример 2.3
def generator(list_):
print('start')
try:
for i in list_:
print('gen')
yield i
except StopIteration:
print('stopped')
def main():
nums = list()
gen = generator(nums)
nums.append(1)
next(gen)
nums.append(2)
for i in gen:
print(i)
nums.append(3)
print(tuple(gen))
next(gen) # raise StopIteration
nums.append(4)
nums.append(5)
main()
input:
start
gen
gen
2
()
Traceback (most recent call last):
in <module>
main()
in main
next(gen)
StopIteration
При преобразовании gen в tuple после полного обхода, мы получаем пустой массив.
Но.. ни один из принтов внутри нашей функции generator при преобразовании не сработал.
Точно так же, как и при вызове next(gen). Принтов нет.
Значит. после первого вызова StopIteration генератор будто сохраняет что-то по типу флага end = True
и при вызове next сразу возвращает StopIteration? Так я подумал сначала
Вот как это объяснила нейронка:
Ваше предположение о том, что генератор сохраняет некоторый флаг end=True
, не совсем точно, но вы правы в том, что после первого вызова StopIteration генератор больше не может быть использован.
Любые дальнейшие вызовы метода next() приведут к немедленному возникновению того же исключения StopIteration, без выполнения какого-либо кода внутри функции-генератора. Такое поведение связано с тем, что генераторы являются одноразовыми объектами, которые нельзя сбросить или перемотать. Как только их итерация завершена, они фактически мертвы и не могут быть использованы снова. Такое поведение является преднамеренным и является фундаментальной частью того, как работают генераторы в Python.
Всё впустую. мы и так уже это знали. В интернете я не нашёл более точного ответа.
Вообще я случайно столкнулся с этим во время работы над телеграмм-ботом на aiogram.
Мне нужен был генератор, который я вызывал бы поочерёдно, получая сообщения, которые мне нужно отправлять и обрабатывать ответ от пользователей. При этом.. мне была необходима динамичность генератора, которую я показал в примере 2.1, так как я допускал, что массив, на котором будет построен генератор, будет пополняться в ходе взаимодействия бота с другими пользователями
Завершая свой монолог, я хотел бы подчеркнуть, что генераторы являются мощным и полезным инструментом. Однако, как и любое другое средство, их следует использовать с осторожностью.