Отличие генераторного выражения от спискового включения Python

Всем привет! Читаю Луца(Изучаем Python, 5-ое издание), возник такой вопрос. В книге сказано, что когда мы вызываем генератор функции или выражения, то он не хранит весь список в памяти, а генерирует на лету. Все понятно, ок. Но не понимаю вот что:

G = (x*2 for x in range(1,5))
print(list(G))

Все равно мы потом, чтобы вывести через print это выражение, мы помещаем его в лист, и это тоже самое, что и списковое включение:

print([x**2 for x in range(1,5)])

Т.е. и там и там список целиком загружается в память. Так где, в каком месте тогда генераторное выражение оптимизирует расход памяти? Если вот здесь:

G = (x*2 for x in range(1,5))

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


Ответы (2 шт):

Автор решения: Евгений

Оптимизация заключается в том, что мы можем расходовать память только тогда, когда нам нужен этот список. Приведу пример. Есть некоторая функция f, которая принимает список, и... Ничего не делает. Важно лишь то, что она принимает список:

def f(l: list):
    pass

И у нас есть большой проект, в котором мы заранее оговариваем, какие списки используются. Что происходит для генератора:

g = (a**2 + 2*a for a in range(10))

# --->
# в этом блоке происходит что-то важное
# здесь какой-то очень большой проект
# впрочем, может, и не очень большой
# --->

f(list(g))

В данном примере память для списка выделяется только в момент вызова функции, а в блоке с каким-то важным кодом лишь небольшой объём памяти выделен для хранения генератора.

Что происходит для списка:

g = [a**2 + 2*a for a in range(10)]

# --->
# в этом блоке происходит что-то важное
# здесь какой-то очень большой проект
# впрочем, может, и не очень большой
# --->

f(g)

А вот в этом примере память для списка выделена сразу, и в момент работы "важного кода" она уже занята, хотя ещё не используется.

→ Ссылка
Автор решения: Vladimir

Здесь надо иметь в виду, что размер небольшого списка может быть меньше, чем размер генератора. Например

import sys

my_list = [1, 2, 3]
print(f'my_list: {my_list}')
list_size_bytes = sys.getsizeof(my_list)
print(f"Размер списка в байтах: {list_size_bytes}") # 88
print(f"Тип переменной: {type(my_list)}") # <class 'list'>
print('-'*100)

my_generator = (i for i in my_list)
print(f'my_generator: {my_generator}')
generator_size_bytes = sys.getsizeof(my_generator)
for i in my_generator:
    print(i)
print(f"Размер генератора в байтах: {generator_size_bytes}") # 200
print(f"Тип переменной: {type(my_generator)}") # <class 'generator'>
print('-'*100)
for i in my_generator:
    print(i)
print(f"Размер генератора в байтах: {generator_size_bytes}") # 200
→ Ссылка