Я пытаюсь разобраться в генераторах и узнать, как их использовать. Я видел много примеров и получаю, что они производят результаты по одному, а не выводят их сразу, как в обычной функции. Но все примеры, которые я видел, связаны с просмотром списка и значений печати, которые генерируются через эту функцию. Что делать, если вы хотите создать список?
Например, я видел пример о четных числах, которые просто генерируют четные числа и распечатывают их, но что, если мне нужен список четных чисел, например:
def even(k):
for i in range(k):
if (i%2):
yield k
even_list = []
for i in even(100):
even_list.append(i)
Означает ли это, что цель использования генератора заключается в том, что он создает это в четном списке. Сохраняет ли этот метод память или время?
Или это метод ниже, без использования генераторов так же эффективно.
def even(k):
evens_list = []
for i in range(k):
if (i%2):
evens_list.append(i)
return evens_list
В этом случае в каких точных случаях полезны генераторы?
Означает ли это, что цель использования генератора заключается в том, что он создает это в четном списке. В этом случае в каких точных случаях полезны генераторы?
Это немного основано на мнениях, но есть ситуации, когда список может не выполнять трюк (например, из-за ограничений оборудования).
Представьте, что у вас есть список четных чисел, а затем хотите взять сумму первых пяти чисел. В Python мы могли бы сделать это с помощью islice
, например:
sumfirst5even = sum(islice(even(100), 5))
Если бы мы сначала сгенерировали список из 100 четных чисел (не зная, что мы позже сделаем с этим списком), то мы потратили много циклов ЦП в построении такого списка, которые теряются.
Используя генератор, мы можем ограничить это только теми элементами, которые нам действительно нужны. Таким образом, мы будем только yield
первые пять элементов. Алгоритм никогда не будет вычислять элементы, размер которых больше 10. Да, здесь представляется сомнительным, что это будет иметь какое-либо (значительное) воздействие. Даже возможно, что для "протокола генератора" потребуется больше циклов ЦП по сравнению с созданием списка, поэтому для небольших списков нет преимущества. Но теперь представьте, что мы использовали even(100000)
, тогда количество "бесполезных циклов ЦП", которые мы потратили на создание целого списка, может быть значительно.
Другим потенциальным преимуществом является сохранение памяти, поскольку нам не нужны все элементы генератора в памяти одновременно.
Возьмем, к примеру, следующий пример:
for x in even(1000):
print(x)
Если even(..)
строит список из 1000
элементов, то это означает, что все эти числа должны быть объектами в памяти одновременно. В зависимости от интерпретатора Python объекты могут принимать значительные объемы памяти. Например, int
принимает в CPython, 28 байт памяти. Это означает, что список, содержащий 500 таких int
может занимать примерно 14 Кбайт памяти (дополнительная память для списка). Да, большинство интерпретаторов Python поддерживают "мухи", чтобы уменьшить нагрузку на малые ints (они разделены, и поэтому мы не создаем отдельный объект для каждого int
мы строим в процессе), но все же он может легко скомпенсироваться. Для even(1000000)
нам потребуется 14 МБ памяти.
Если мы используем генератор, чем в зависимости от того, как мы используем генератор, мы можем сэкономить память. Зачем? Поскольку после того, как нам больше не нужно число 123456
(поскольку цикл for
переходит к следующему элементу), пространство, в котором объект "занято", может быть переработано и передано объекту int
со значением 12348
. Таким образом, это означает, что - учитывая то, как мы используем генератор, это позволяет - использование памяти остается постоянным, тогда как для списка оно масштабируется линейно. Конечно, сам генератор также нуждается в правильном управлении: если в коде генератора мы создадим коллекцию, то, разумеется, увеличится и память.
В 32-разрядных системах это может привести к некоторым проблемам, поскольку списки Python имеют максимальную длину. Список может содержать не более 536'870'912 элементов. Да, это огромное количество, но что, если вы, например, хотите сгенерировать все перестановки данного списка? Если мы сохраним перестановки в списке, то это означает, что для 32-битной системы, списка из 13 (или более элементов), мы никогда не сможем построить такой список.
В теоретическом информатике "онлайн-алгоритм" определяется некоторыми исследователями как алгоритм, который получает вход постепенно и, следовательно, не знает весь ввод заранее.
Практическим примером может быть веб-камера, которая каждую секунду создает изображение и отправляет его на веб-сервер Python. В тот момент мы не знаем, как будет выглядеть изображение, которое будет захвачено веб-камерой в течение 24 часов. Но мы могли бы быть заинтересованы в обнаружении взломщика, который стремится украсть что-то. В этом случае список кадров, таким образом, не будет содержать все изображения. Однако генератор может построить элегантный "протокол", где мы итеративно извлекаем изображение, обнаруживаем взломщика и поднимаем сигнал тревоги, например:
for frame in from_webcam():
if contains_burglar(frame):
send_alarm_email('Maurice Moss')
Нам не нужны веб-камеры или другое оборудование, чтобы использовать элегантность генераторов. Генераторы могут давать "бесконечную" последовательность. Или even
генератор может, например, выглядеть следующим образом:
def even():
i = 0
while True:
yield i
i += 2
Это генератор, который в итоге генерирует все четные числа. Если мы продолжим итерацию над этим, в конце концов мы дадим число 123'456'789'012'345'678 (хотя это может занять очень много времени).
Вышеприведенное может быть полезно, если мы хотим реализовать программу, которая, например, продолжает давать четные числа, являющиеся палиндромами. Это может выглядеть так:
for i in even():
if is_palindrome(i):
print(i)
Таким образом, мы можем предположить, что эта программа будет работать, и не нужно "обновлять" список четных чисел. На некоторых чистых функциональных языках, которые делают ленивое программирование прозрачным, программы записываются так, как будто вы создаете список, но на самом деле он обычно является генератором.
range(..)
и друзья В Python многие классы не строят списки, когда вы перебираете их, например, объект range(1000)
сначала не строит список (он работает в python-2.x, но не в python-3.x). Объект range(..)
просто представляет диапазон. Объект range(..)
не является генератором, но это класс, который может генерировать объект итератора, который работает как генератор.
Помимо итерации, мы можем делать всевозможные вещи с помощью объекта range(..)
, который возможен со списками, но не эффективным образом.
Если мы, например, хотим знать, является ли 1000000000
элементом range(400, 10000000000, 2)
, тогда мы можем написать 1000000000 in range(400, 10000000000, 2)
. Теперь есть алгоритм, который будет проверять это без создания диапазона или построения списка: он видит, являются ли элементы int
, находится в диапазоне объекта range(..)
(поэтому больше или равно 400
, и менее 10000000000
), и будет ли он получен (принимая во внимание этот шаг), это не требует итерации по нему. В результате проверка членства может быть выполнена мгновенно.
Если бы мы сгенерировали список, это означало бы, что Python должен был перечислить каждый элемент, пока он наконец не сможет найти этот элемент (или достигнет конца списка). Для таких чисел, как 1000000000
, это может занять несколько минут, часов или дней.
Мы также можем "срезать" объект диапазона, который дает другой объект range(..)
, например:
>>> range(123, 456, 7)[1::4]
range(130, 459, 28)
с помощью алгоритма мы можем таким образом мгновенно разрезать объект range(..)
в новый объект range
. Нарезка списка занимает линейное время. Это может снова (для огромных списков) занять значительное время и память.
Генераторы короче и читабельны:
В вашем примере вам нужно создать пустой список, использовать append
и вернуть полученный список:
def even(k):
evens_list = []
for i in range(k):
if i % 2 != 0:
evens_list.append(i)
return evens_list
Генератору нужен только yield
:
def even(k):
for i in range(k):
if i % 2 != 0:
yield i
И использование почти то же самое, если вам действительно нужен список. Вместо
event_list = even(100)
линия становится
event_list = list(even(100))
Генератор, но в целом ленивая семантика, дает некоторые преимущества:
Но и некоторые недостатки:
Вы можете легко и эффективно преобразовать вывод генератора в список со list()
:
even_list = list(even(100))
range
возвращает генератор, а не список в Python 3 против Python 2. Код OP преобразует выходные данные генератора в список гораздо менее эффективным способом. Отсюда и мой ответ.
for i in even(100):
генерировать всю последовательность за один раз в любом случае