Что такое генераторы

При стандартном подходе используются функции, которые возвращают списки. Допустим, вот такая функция, которая возвращает все степени двойки до 1000:

def degrees_two():
  result = []
  i = 0
  while 2**i <= 1000:
      result.append(2**i)
      i += 1
  return result

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

Минусы генераторов

Но у генераторов также есть и минусы:

  • Во-первых, генератор можно использовать ТОЛЬКО ОДИН РАЗ. После этого при попытке его использовать просто ничего не произойдёт. Это создаёт минус там, где надо повторять действие много раз.

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

Метрики

Всего я буду измерять по двум метрикам: время выполнения и место в памяти. Измерять буду генератор, уже проитерированный в цикле генератор и обычная функция. Для начала импортируем библиотеки замера времени и размера: sys и time. Использоваться будут функции sys.getsizeof() и time.time(). Напишем функцию для поиска делителей:

def primes(n):
    result = list()
    for i in range(1, n//2+1):
        if n%i == 0:
            result.append(i)
    return result

И функцию-генератор, которая возвращает числа при помощи ключевого слова yield:

def gen_primes(n):

for i in range(1, n//2+1):

if n%i == 0:

yield i

Далее создадим собственно объекты замера и вывод:

start = time.time()
example = primes()
end = time.time()
time1 = int(round((end-start)*1000000))

start_gen = time.time()
example_gen = [x for x in gen_primes()]
end_gen = time.time()
time2 = int(round((end_gen-start_gen)*1000000))
print(f"Метрика 1: Время \n\tОбычная функция: {time1} микросекунд.\n\tГенератор: {time2} микросекунд.")

size1 = sys.getsizeof(example)
size2 = sys.getsizeof(example_gen)
print(f"Метрика 2: Размер \n\tОбычная функция: {size1} байт.\n\tГенератор: {size2} байт.")

В тестировании будет 3 раунда:

  • Раунд 1 - 1000

  • Раунд2 - 100тыс.

  • Раунд 3 - 10млн

Запускаем первый раунд и видим вывод:

Метрика 1: Время Обычная функция: 105 микросекунд. Генератор: 80 микросекунд.

Метрика 2: Размер

Обычная функция: 184 байт.

Генератор: 184 байт.

Как видим, проитерированный генератор выполнился быстрее, но память расходована одинаково.

Запускаем второй раунд, со значением 100 тысяч и встречаем вывод:

Метрика 1: Время Обычная функция: 16376 микросекунд. Генератор: 14217 микросекунд.

Метрика 2: Размер

Обычная функция: 376 байт.

Генератор: 376 байт.

Разница во времени и осталась ~⅛. Запустим 3 раунд, значение - 10 миллионов:

Метрика 1: Время Обычная функция: 1148705 микросекунд. Генератор: 1161036 микросекунд.

Метрика 2: Размер

Обычная функция: 568 байт.

Генератор: 568 байт.

В обоих случаях время примерно секунда с лимним, правда, тут генератор начал отставать. При повторном запуске, наоборот, генератор был быстрее:

Метрика 1: Время Обычная функция: 1263085 микросекунд. Генератор: 1130301 микросекунд.

Метрика 2: Размер

Обычная функция: 568 байт.

Генератор: 568 байт.

Причем как раз на одну восьмую. Поэтому, для чистоты эксперимента я дополнительно попробовал эти функции в 1 миллиарде чисел. Итак, барабанная дробь, вот:

Метрика 1: Время

Обычная функция: 113 820 924 микросекунд.

Генератор: 115 045 928 микросекунд.

Метрика 2: Размер

Обычная функция: 920 байт.

Генератор: 920 байт.

В итоге генератор отстал. Это длилось почти две минуты. Перезапускать я не стал, если хотите, попробуйте сами.

Теперь займусь генератором ещё не проитерированным:

Метрика 1: Время Обычная функция: 196 микросекунд. Генератор: 5 микросекунд.

Метрика 2: Размер Обычная функция: 184 байт. Генератор: 216 байт.

Как ни странно, в таком виде генератор занимает больше, чем функция. А по времени он "успел" на 5 микросекунд. То есть большую часть времени занимает именно итерирование циклом. Но при этом в последующих раундах этот размер и время остаются стабильными, когда у обычных функций растёт.

Итог

Итак, генераторы выигрывают по времени от функций, но незначительно, а на больших "высотах" и то не всегда. Памятью уже проитерированные одинаковы с функциями. Но просто генератор создаётся очень быстро и имеет фиксированный объём в памяти, будь параметром хоть 10, хоть миллиард.