Страницы

Поиск по вопросам

четверг, 13 февраля 2020 г.

В каком виде в оперативной памяти хранятся генераторы?

#python


В каком виде в оперативной памяти храниться генератор при итерации по нему?

Например:

res = (num for num in range(10**100000)
for el in res:
    print(el)


Понятно, что сам генератор храниться как объект в оперативной памяти и занимает некоторое
определенное место. Но что происходит при непосредственной итерации по нему? Какую
память начинает занимать сам генератор res после 1-ой, 2-ой и так далее итераций?
    


Ответы

Ответ 1



res = (num for num in range(10**100000) приблизительно соответствует следующему коду def f(): for num in range(10**100000) yield num res = f() Здесь f – это сопрограмма, специальная функция, которая приостанавливает выполнение после каждого yield, давая возможность вызывающей стороне забрать сгенерированное значение, а также передать какое-нибудь значение внутрь функции. Сопрограмма возобновит выполнение с того места, где остановилась, когда вызывающая сторона попросит следующее значение. Если значений не осталось (сопрограмма завершилась), она выбросит исключение StopIteration, которое воспринимается как сигнал к завершению цикла. def f(): for i in range(3): x = yield i * i print('получено', x) >>> coro = f() >>> coro.send(None) 0 >>> coro.send(11) получено 11 1 >>> coro.send(True) получено True 4 >>> coro.send('test') получено test Traceback (most recent call last): File "", line 1, in StopIteration Другими словами, генераторное выражение хранится в виде вызванной, но не завершенной функции, и занимает примерно столько памяти, сколько требуется на хранение самого функционального объекта и всех локальных переменных вызванной функции. С range дела обстоят немного иначе. Это объект, который хранит три значения – начало, конец и шаг и на их основе позволяет вычислить любое значение из диапазона по формуле ri = начало + i ∙ шаг или ri = конец + i ∙ шаг если индекс отрицательный. А при взятии среза создается новый объект range >>> r = range(0, 1000000, 1000) >>> r[0] 0 >>> r[-1] 999000 >>> r[1:10] range(1000, 10000, 1000) >>> r[::-1] range(999000, -1000, -1000) Когда такой объект передается в for, создается итератор, который дополнительно хранит еще и индекс текущего элемента. >>> r = range(3) >>> it = iter(r) >>> it >>> next(it) 0 >>> next(it) 1 >>> next(it) 2 >>> next(it) Traceback (most recent call last): File "", line 1, in StopIteration Т.е. range, так же как и генераторное выражение не нуждается в том, чтобы хранить все значения в памяти, он просто высчитывает очередное значение при каждом обращении

Ответ 2



Размер занимаемый генератором в памяти, не должен сильно меняться. Пример: In [119]: gen = (num for num in range(10**100000)) In [120]: sys.getsizeof(gen) Out[120]: 88 In [121]: gen.__next__() Out[121]: 0 In [122]: gen.__next__() Out[122]: 1 In [123]: gen.__next__() Out[123]: 2 In [124]: sys.getsizeof(gen) Out[124]: 88 In [125]: for i in range(10000): ...: gen.__next__() ...: In [126]: sys.getsizeof(gen) Out[126]: 88

Ответ 3



Генераторное выражение, это, по сути, просто итератор (то есть то, что идёт после in) плюс информация о том, что нужно сделать с очередным элементом (то есть часть ... for ... и, если она есть, то ещё часть с if). То есть когда от генератора требуется очередной элемент, ему нужно просто дернуть очередной элемент из итератора, проверить, что оно совпадает с условием if (если оно есть), и выполнить какую-то обработку, которая описана в ... for .... Соответственно, никакой дополнительной памяти не нужно в процессе перебора элементов генератора.

Комментариев нет:

Отправить комментарий