Работая с генераторами через 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, так как я допускал, что массив, на котором будет построен генератор, будет пополняться в ходе взаимодействия бота с другими пользователями

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

Комментарии (12)


  1. mayorovp
    26.04.2024 12:35
    +8

    Так много слов, когда можно было написать проще:

    Генераторы однопроходны и не могут быть переитерированы. Точка.


    1. klis
      26.04.2024 12:35
      +3

      Зачем вообще это писать? Это же прямо их определение…


      1. lorc
        26.04.2024 12:35
        +2

        Ну автор вон целый пост настрочил, не разобравшись.


        1. unreal_undead2
          26.04.2024 12:35

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


          1. klis
            26.04.2024 12:35

            Да, кейс жизненный. Только статью на хабре зачем про это писать? Это же база. Типа «вообще я сантехник, но тут довелось поразбираться с электрикой, и вот что оказывается будет, если рукой взяться за фазу, а ногой заземлиться, я и так попробовал и сяк, но результат один и тот же… напишу я статью про это в журнал Радио, а то вдруг они не в курсе»


    1. IDeSeI Автор
      26.04.2024 12:35

      Конечно, если всё это изначально знать, разъяснения будут казаться глупыми. Но подумайте о тех, кто в этом ещё не разобрался. Вряд ли они смогут понять всю суть по одному предложению.


  1. Andrey_Solomatin
    26.04.2024 12:35
    +7

    Мне нужен был генератор, который я вызывал бы поочерёдно, получая
    сообщения, которые мне нужно отправлять и обрабатывать ответ от
    пользователей


    Зачем для этого генератор? Почему не очередь? Например: https://docs.python.org/3/library/collections.html#collections.deque

    Однако, как и любое другое средство, их следует использовать с осторожностью.

    Я бы сказал для решения задач, для которых он предназначен.


    1. CrazyElf
      26.04.2024 12:35
      +2

      Вот-вот, тут явно очередь напрашивается. Использовать для задачи неподходящую коллекцию, а потом ругаться, что она не так работает - странновато ))


      1. IDeSeI Автор
        26.04.2024 12:35

        Я не ругался. Просто решил разобраться, почему так, и заодно объяснить это тем, кто этого не знал. Для кого-то это банально и просто, а для кого-то это может быть полезно


        1. CrazyElf
          26.04.2024 12:35

          Заголовок статьи что называется misleading. Это не проблема, это фича. В этом и есть проблема вашей статьи.


    1. IDeSeI Автор
      26.04.2024 12:35

      Я старался не создавать полноценные массивы, ведь их может быть очень много и это сказалось бы на нагрузке системы.


  1. milssky
    26.04.2024 12:35
    +4

    Когда-то давно, когда трава была зеленее и все такое, каждому джуну рекомендовали прочитать книжку Fluent Python.

    Вам тоже рекомендую :)