В каких ситуациях вы должны использовать генераторы в Python?

1

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

Например, я видел пример о четных числах, которые просто генерируют четные числа и распечатывают их, но что, если мне нужен список четных чисел, например:

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

В этом случае в каких точных случаях полезны генераторы?

  • 0
    Обычно для экономии памяти (не все элементы должны существовать одновременно) или времени (если вы часто думаете, что необходимы только первые k элементов), или для вещей, которые не заканчиваются (например, поток веб-камеры).
  • 0
    @WillemVanOnsem Я думаю, что вопрос в большей степени направлен на то, будет ли for i in even(100): генерировать всю последовательность за один раз в любом случае
Показать ещё 2 комментария
Теги:
iterator
generator

4 ответа

4
Лучший ответ

Означает ли это, что цель использования генератора заключается в том, что он создает это в четном списке. В этом случае в каких точных случаях полезны генераторы?

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

Сохранение циклов процессора (время)

Представьте, что у вас есть список четных чисел, а затем хотите взять сумму первых пяти чисел. В 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) сначала не строит список (он работает в , но не в ). Объект 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. Нарезка списка занимает линейное время. Это может снова (для огромных списков) занять значительное время и память.

1

Генераторы короче и читабельны:

В вашем примере вам нужно создать пустой список, использовать 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))
1

Генератор, но в целом ленивая семантика, дает некоторые преимущества:

  • Вы можете создать бесконечный список
  • Вы можете сэкономить много памяти, потому что она не сохраняет в памяти весь список
  • Часто используется для дорогостоящих операций ввода-вывода, поэтому вы можете эффективно извлекать данные только тогда, когда вы действительно используете их

Но и некоторые недостатки:

  • накладные расходы
    • Вам нужно сохранить в памяти переменные функции генератора
    • также риск утечки памяти
  • Каждый раз, когда вы хотите повторно использовать элементы в списке, он должен быть регенерирован
  • 0
    В моем примере выше он хранит список событий в памяти. Итак, я думаю, мой вопрос был ли метод генератора лучше, чем оригинальный метод выше?
  • 0
    Зависит от двух факторов: размер списка и время использования этого списка. Для списка из 100 элементов я думаю, что нет проблем с пространством. Таким образом, последний фактор - это время, когда вы используете этот список четных чисел, если вы используете один раз, вы можете использовать генератор, иначе я предлагаю вам список
Показать ещё 3 комментария
-1

Вы можете легко и эффективно преобразовать вывод генератора в список со list():

even_list = list(even(100))
  • 0
    Тогда это побеждает точку генератора. Это не то, что спрашивает ОП
  • 0
    Генератор дает гибкость выбора без затрат на эффективность. Это причина, по которой range возвращает генератор, а не список в Python 3 против Python 2. Код OP преобразует выходные данные генератора в список гораздо менее эффективным способом. Отсюда и мой ответ.
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню