Генераторы Python и объяснение доходности: экономьте память с помощью реальных примеров

  1. Главная
  2. Блог
  3. Разработка программного обеспечения

Разработка программного обеспечения

Генераторы Python и объяснение доходности: Экономьте память с помощью реальных примеров

Генераторы создают значения по одному, а не создают в памяти целый список. Посмотрите, как доходность приостанавливает функцию, посмотрите разницу в памяти, которую мы измерили, и создайте небольшой конвейер данных.

9 мин чтения
Объяснение генераторов Python и доходности: экономьте память на реальных примерах

Главное

  • Функция, содержащая yield, возвращает генератор, и ее тело не запускается, пока вы не запросите значение.
  • yield приостанавливает функцию и сохраняет ее локальное состояние до следующего запроса.
  • Генераторы используют небольшой фиксированный объем памяти, сколько бы значений они ни выдавали.
  • Генератор можно использовать только один раз, что является наиболее распространенной ловушкой.

Представьте себе, что вы загружаете файл журнала размером 10 ГБ в список только для того, чтобы найти ошибки. Вашей программе может не хватить памяти прежде, чем она ее найдет. Генератори Python решают эту проблему, генерируя значения по требованию, по одному, поэтому вы можете работать с огромными или даже бесконечными данными практически без памяти.

В этом руководстве шаг за шагом показано, что на самом деле происходит при работе генератора. Мы запускали каждый пример на Python 3.13, и в результате этого запуска были получены выходные данные. Это помогает уже освоиться с функциями и циклами, о которых рассказывается в нашем руководстве по Python для начинающих.

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

Генератор — это особый вид итератора, который создается путем написания функции с использованием yield. Любая функция с yield автоматически становится функцией-генератором, и ее вызов дает вам объект-генератор, а не запускает код.

def count_up(limit):
    print("starting")
    n = 1
    while n <= limit:
        yield n
        n += 1
    print("finished")

gen = count_up(2)
print(gen)

# <generator object count_up at 0x...>

Обратите внимание, что «запуск» не напечатан. Вызов функции только создал генератор. Внутри еще ничего не запустилось.

Как работает доходность, шаг за шагом

Каждый вызов next() запускает функцию до тех пор, пока она не достигнет yield, возвращает это значение и зависает на месте. Следующий вызов возобновляется сразу после этой строки, при этом все локальные переменные остаются неизменными.

print(next(gen))
# starting
# 1

print(next(gen))
# 2

next(gen)
# finished
# StopIteration

Внимательно следите за выводом. «Запуск» появляется только на первом next(). Во втором случае выполнение продолжается с того места, где оно было остановлено, поэтому n все еще доступно и становится равным 2. В третьем цикл заканчивается, выводится сообщение «finished», функция падает с конца, и Python поднимает StopIteration, чтобы сигнализировать, что ничего не осталось.

Вы редко звоните в next() сами. Цикл for делает это за вас и спокойно обрабатывает StopIteration, а также list(), sum() и подобные функции:

print(list(count_up(3)))

# starting
# finished
# [1, 2, 3]

доходность против доходности

Аспект возвращение урожайность
Что он делает Завершает функцию Приостанавливает функцию
Произведенные ценности Один результат Серия, по одному
Местный штат Выброшено Сохраняется между значениями
Когда код запускается Сразу при звонке Только когда запрашивается значение
Память на множество значений Должен держать их всех Держится по одному

Эта задержка известна как ленивая оценка: значения вычисляются только при необходимости. Обычный return внутри генератора просто завершает его, что запускает StopIteration.

Разница в памяти, измеренная

Мы сравнили список из миллиона квадратов с эквивалентным генератором, используя sys.getsizeof:

import sys

squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))

print(sys.getsizeof(squares_list), sys.getsizeof(squares_gen))

# 8448728 200

Собственная структура списка занимала около 8,4 МБ, тогда как генератор в нашей системе занимал всего 200 байт. Реальный разрыв еще больше, поскольку getsizeof не учитывает целочисленные объекты внутри списка. Размер генератора также остается неизменным для миллиарда элементов, поскольку он сохраняет только текущее состояние, а не результаты.

Однако есть компромисс. Список можно повторно использовать, индексировать и измерять с помощью len(). Генератор не предлагает ничего из этого. Если вам нужен произвольный доступ или вы несколько раз выполняете цикл по небольшому объему данных, список проще. Дополнительную информацию об этих типах см. в нашем руководстве по списку Python, кортежу и набору.

Выражения-генераторы

В простых случаях вам не нужна функция. Выражение-генератор выглядит как список с круглыми скобками вместо квадратных:

squares = [n * n for n in range(5)]   # builds a list now
squares = (n * n for n in range(5))   # produces values on demand

print(sum(n * n for n in range(5)))

# 30

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

Создайте конвейер данных

Вот где генераторы действительно блестят. Небольшие генераторы можно объединить в цепочку, чтобы каждый элемент проходил каждый шаг по одному:

def read_lines(lines):
    for line in lines:
        yield line.strip()

def only_errors(lines):
    for line in lines:
        if line.startswith("ERROR"):
            yield line

def messages(lines):
    for line in lines:
        yield line.split(" ", 1)[1]

log = ["INFO started\n", "ERROR db down\n",
       "ERROR disk full\n", "INFO stopped\n"]

print(list(messages(only_errors(read_lines(log)))))

# ['db down', 'disk full']

Каждый шаг выполняет одну небольшую работу и может быть протестирован отдельно. В реальном использовании замените список образцов на открытый файл, поскольку объекты файла можно читать построчно, как показано в нашем руководстве по Обработке файлов Python. Даже журнал размером в несколько гигабайт будет проходить с очень небольшим объемом памяти.

Совет: Конвейер не работает, пока что-то его не использует. Построение цепочки происходит мгновенно. Обработка происходит только тогда, когда вы зацикливаете окончательный генератор или помещаете его в вызов типа list().

Бесконечные последовательности

Список не может быть бесконечным, а генератор может, потому что он всегда вычисляет только следующее значение. Используйте itertools.islice, чтобы взять столько, сколько вам нужно:

import itertools

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(itertools.islice(fibonacci(), 8)))

# [0, 1, 1, 2, 3, 5, 8, 13]

Однако будьте осторожны: вызов list(fibonacci()) без ограничений будет выполняться до тех пор, пока у вас не закончится память. Тот же шаблон подходит для генераторов идентификаторов, циклов опроса и результатов API с разбивкой на страницы, где вы получаете следующую страницу только тогда, когда текущая заканчивается.

урожайность и партии

yield from переходит к другой итерации, сохраняя вложенный цикл:

def chain_all(*groups):
    for group in groups:
        yield from group

print(list(chain_all([1, 2], (3, 4))))

# [1, 2, 3, 4]

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

def batches(items, size):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch

print(list(batches(range(7), 3)))

# [[0, 1, 2], [3, 4, 5], [6]]

Финал if batch гарантирует, что меньшая оставшаяся группа не будет потеряна. Python 3.12 и более поздние версии также включают itertools.batched, который выполняет аналогичную работу.

отправить, закрыть и очистить

Генераторы также могут получать значения. send() возобновляет работу генератора, и значение становится результатом выражения yield. Вы должны сначала вызвать next() один раз, чтобы достичь первого yield:

def running_total():
    total = 0
    while True:
        value = yield total
        total += value

t = running_total()
next(t)
print(t.send(10), t.send(5))

# 10 15

Большинству людей это редко нужно. Более распространенной проблемой является очистка. Если вы остановитесь раньше, блок finally все равно будет работать, когда генератор закрыт:

def tracked():
    try:
        yield 1
        yield 2
    finally:
        print("cleanup ran")

tr = tracked()
next(tr)
tr.close()

# cleanup ran

Pytest использует именно эту идею: фикстура с yield после этого запускает очистку, как показано в нашем руководстве по pytest. Аналогично, декоратор contextmanager стандартной библиотеки превращает генератор в блок with.

Генераторы в классах

Написание метода __iter__ в качестве генератора — это самый простой способ сделать ваш собственный класс итерируемым. Бонусом является то, что каждый цикл получает новый генератор, поэтому объект можно повторять снова и снова:

class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

c = Countdown(3)
print(list(c), list(c))

# [3, 2, 1] [3, 2, 1]

Когда использовать генератор

Используйте генератор, когда Используйте список, когда
Данные большие или их размер неизвестен Данные небольшие и помещаются в память
Вы обрабатываете элементы один раз по порядку Вы повторяетесь несколько раз
Вы можете остановиться раньше Вам нужно len(), индексирование или нарезка
Вы читаете поток или файлы Вам нужно отсортировать или перевернуть данные
Вы строите трубопровод Простота важнее памяти

Генератор в целом не всегда быстрее. Если в конечном итоге вы будете использовать все значения, общая работа будет аналогична, а понимание списка может быть даже быстрее для небольших данных, поскольку оно требует меньше накладных расходов на каждый элемент. Основными преимуществами являются использование памяти и более быстрое получение первых результатов.

Распространенные ошибки

Повторное использование отработанного генератора

В какой-то момент это ловит почти всех. Генератор запоминает, что он завершен:

g = count_up(2)
print(list(g), list(g))

# [1, 2] []

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

Ожидание немедленного запуска кода

def noisy():
    print("body runs")
    yield 1

n = noisy()        # nothing is printed
print(next(n))     # body runs, then 1

Проверка аргументов в верхней части генератора не запускается до первого next(), поэтому ошибки могут появляться далеко от вызова, который их вызвал.

Другие ловушки

  • Вызов len() или индексирование. Генераторы не поддерживают ни того, ни другого и вызывают TypeError.
  • Слишком раннее закрытие файла. Возврат генератора из блока with может означать, что файл закроется до того, как будет прочитана какая-либо строка. Поместите yield внутрь блока with.
  • Обернуть все в list(). Это сводит на нет преимущества памяти.
  • Используйте один для небольших данных. Простой список часто более понятен.

Итоги

Генератор — это функция, которая может приостанавливать работу. Одна эта идея дает вам низкое использование памяти, бесконечные последовательности и чистые конвейеры. Чтобы применить это на практике, найдите место в своем коде, которое создает большой список только для одного цикла, и превратите его в генератор. Затем попробуйте объединить два небольших генератора в реальный файл журнала.

Часто задаваемые вопросы

Что делает выход в Python?

доходность создает значение и приостанавливает функцию, сохраняя ее локальные переменные. Функция возобновляет работу с той же точки, когда запрашивается следующее значение.

В чем разница между генератором и списком?

Список хранит в памяти все свои значения одновременно. Генератор вычисляет каждое значение по требованию, использует мало памяти и обычно может быть использован только один раз.

В чем разница между итератором и генератором?

Каждый генератор является итератором. Генератор — это просто самый простой способ его написания, поскольку Python создает для вас методы __iter__ и __next__.

Можете ли вы дважды переключить генератор?

Нет. После исчерпания он больше ничего не дает, не вызывая ошибки. Создайте новый генератор, вызвав функцию еще раз, или сохраните результаты в списке.

Генераторы быстрее списков?

Не всегда. Они используют гораздо меньше памяти и быстрее выдают первый результат, но понимание списка может быть немного быстрее, если вы полностью потребляете небольшие объемы данных.

Продолжайте учиться

Работайте с Stack Quora

Нужна помощь с тестированием, WordPress или SEO?

Мы помогаем командам выпускать лучшее программное обеспечение и развивать свои сайты. Расскажите нам, над чем вы работаете, и мы предложим четкий следующий шаг.

Продолжайте читать

Больше руководств по тестирование, искусственный интеллект и современные разработки

Понравилась эта статья? Выберите тему и сразу приступайте к нашим новейшим руководствам.

Искусственный интеллект

Объяснение галлюцинаций искусственного интеллекта: почему языковые модели все выдумывают и как их уменьшить

Галлюцинации ИИ объяснили: что это такое, почему языковые модели их создают, распространенные типы, как уменьшить их с помощью заземления и подсказо…

Читать статью →
DevOps

Объяснение переменных среды: файлы .env, секреты и правильно выполненная конфигурация

Переменные среды объясняются на примерах Python, которые мы запускали: их чтение и установка, почему каждое значение является строкой, файлы .env, н…

Читать статью →
Разработка ПО

Объяснение ограничения скорости API: алгоритмы, заголовки и обработка ошибок 429

Ограничение скорости API объясняется с помощью проведенного нами моделирования: сегмент токенов, фиксированное окно, скользящее окно, 429 ответов, з…

Читать статью →
Автоматизация тестирования

Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

Издевательство на Python с помощью unittest.mock, протестировано: Mock и return_value, Side_effect, исправление использования имени, спецификация и …

Читать статью →
Тестирование ПО

Объяснение покрытия кода: покрытие линий или ветвей и что на самом деле означает 100%

Покрытие кода объяснено с помощью реального запуска покрытия.py: покрытие строк, ветвей и условий, чтение отчета, почему 100%-ное покрытие все еще м…

Читать статью →
Базы данных и SQL

DELETE, TRUNCATE и DROP в SQL: различия, откат и когда использовать каждый из них

Объяснение DELETE, TRUNCATE и DROP на примере протестированного SQL: что каждый из них удаляет, откат, триггеры, сброс идентификаторов, скорость и ч…

Читать статью →
Базы данных и SQL

Объяснение представлений SQL: создавайте, используйте и избегайте ловушек, с примерами

Представления SQL, объясненные с помощью запросов, которые мы выполняли: создание представления, результаты в реальном времени, скрытие столбцов, пр…

Читать статью →
Разработка ПО

Интерфейс TypeScript и тип: реальные различия, протестированные с помощью компилятора

Интерфейс TypeScript против типа, проверенный реальным компилятором: что могут делать оба, что может делать только один, слияние объявлений, расшире…

Читать статью →
Разработка ПО

Объяснение Python *args и **kwargs: гибкие функции с понятными примерами

Python *args и **kwargs, объясненные с помощью кода, который мы запускали: упаковка и распаковка, порядок параметров, аргументы только по ключевым с…

Читать статью →
Разработка ПО

Лямбда-функции Python: синтаксис, реальное использование и когда их не следует использовать

Лямбда-функции Python, объясненные с помощью кода, который мы запускали: синтаксис, сортировка с помощью ключа, карты и фильтра, замыкания, ловушка …

Читать статью →
Искусственный интеллект

Что такое ИИ-агенты? Как они работают: можно запустить простой цикл агента

Что такое агенты ИИ? Простое руководство по циклу агента, инструментам, памяти и ограничениям, с небольшим циклом Python, который мы запустили, реал…

Читать статью →
DevOps

Мониторинг и наблюдаемость: реальная разница с журналами, метриками и трассировками

Объяснение мониторинга и наблюдаемости: известные и неизвестные сбои, журналы, метрики и трассировки, пошаговое руководство по инцидентам при оформл…

Читать статью →
DevOps

Docker Volumes против Bind Mounts: правильное сохранение данных контейнера

Объяснение томов Docker и монтирования привязки: почему исчезают данные контейнера, именованные тома, монтирование привязки, tmpfs, примеры создания…

Читать статью →
Автоматизация тестирования

TDD против BDD: ключевые различия, одна функция, созданная для обоих способов, и когда использовать каждый

TDD и BDD объясняются с помощью одной функции скидки, созданной в обоих направлениях: красно-зеленый рефакторинг с кодом, который мы запускали, сцен…

Читать статью →
Тестирование ПО

Пользовательское приемочное тестирование (UAT): процесс, контрольный список и реальные примеры

Приемочное тестирование пользователей объяснило: кто его проводит, чем оно отличается от контроля качества, семиэтапный процесс, шаблон тестового сц…

Читать статью →
Базы данных и SQL

Объяснение значений NULL в SQL: IS NULL, COALESCE, NULLIF и ловушки, которых следует избегать

Значения SQL NULL объяснены с помощью запросов, которые мы выполнили: трехзначная логика, IS NULL, COALESCE, NULLIF, как агрегаты, соединения и сорт…

Читать статью →
Базы данных и SQL

Оператор SQL CASE WHEN: синтаксис, реальные примеры и распространенные ошибки

SQL CASE WHEN объяснен с помощью запросов, которые мы выполняли: простые формы и формы для поиска, порядок условий, повороты с помощью SUM, трюки CO…

Читать статью →
Разработка ПО

Что такое JWT? Веб-токены JSON, объясненные на рабочем примере

Что такое JWT? Объяснение JSON Web Tokens: три части: токен, созданный и измененный в коде, который мы запускали, JWT и сеансы, хранилище, токены об…

Читать статью →
Разработка ПО

Объяснение цикла событий JavaScript: стек вызовов, микрозадачи и таймеры с примерами

Цикл событий JavaScript объясняется с помощью кода, который мы запускали: стек вызовов, очереди задач и микрозадач, почему setTimeout 0 не является …

Читать статью →
Разработка ПО

Генераторы Python и объяснение доходности: экономьте память с помощью реальных примеров

Генераторы Python и доходность, объясненные с помощью кода, который мы запускали: как они приостанавливаются и возобновляются, измеряют экономию пам…

Читать статью →
Искусственный интеллект

Что такое вложения? Простое объяснение векторов и косинусного сходства

Что такое вложения? Простое руководство по векторам, косинусному подобию и семантическому поиску с небольшим примером, который мы использовали, общи…

Читать статью →
DevOps

Сине-зеленое и канареечное развертывание: различия, компромиссы и как выбрать

Объяснение сине-зеленого и канареечного развертывания: как каждый из них работает, стоимость, риски и откат, стабильное группирование пользователей,…

Читать статью →
Автоматизация тестирования

Ненадежные тесты: что их вызывает и как их исправить навсегда

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

Читать статью →
Тестирование ПО

Нагрузочное тестирование и стресс-тестирование: различия, метрики и примеры k6

Объяснение нагрузочного и стресс-тестирования: что доказывает каждое из них, тесты на всплески и выдержки, важные метрики, почему средние значения в…

Читать статью →
Тестирование ПО

Тестовый пример и тестовый сценарий: ключевые различия, примеры и когда использовать каждый

Объяснение тестового случая и тестового сценария: четкие определения, параллельная таблица, одна функция сброса пароля, взятая из сценариев в случаи…

Читать статью →
Базы данных и SQL

Оптимизация SQL-запросов: чтение планов выполнения и исправление медленных запросов с указанием времени

Оптимизация SQL-запросов проверена на миллионе строк: читайте планы выполнения, добавляйте правильные индексы, исправляйте неразмещаемые фильтры, ис…

Читать статью →
Базы данных и SQL

SQL UNION против UNION ALL: различия, примеры и проверка производительности

SQL UNION и UNION ALL объяснены с помощью запросов, которые мы выполнили: дубликаты, правила, которым следуют оба, реальная ошибка данных, измеренна…

Читать статью →
Разработка ПО

Учебное пособие по API JavaScript Fetch: GET, POST, обработка ошибок и тайм-ауты

Учебное пособие по API JavaScript Fetch, протестированное на локальном сервере: GET и POST, почему не выдается ошибка 404, многоразовый помощник, ст…

Читать статью →
Разработка ПО

Обработка файлов Python: чтение, запись, CSV, JSON и pathlib с примерами

Обработка файлов Python объясняется с помощью кода, который мы запускали: открытые режимы, чтение больших файлов, CSV, JSON, pathlib, двоичные файлы…

Читать статью →
Разработка ПО

Классы и объекты Python: учебник ООП с реальными примерами

Классы и объекты Python, объясненные с помощью кода, который мы запускали: __init__, методы, атрибуты класса и экземпляра, наследование, свойства, м…

Читать статью →
Искусственный интеллект

Что такое большая языковая модель? Как работают LLM, объяснено без шумихи

Что такое большая языковая модель? LLM объясняется простым языком: токены, встраивания, внимание, обучение, температурные и контекстные окна, крошеч…

Читать статью →
DevOps

Команды Linux для начинающих: основное руководство по терминалу с примерами

Команды Linux для начинающих, сгруппированные по задачам: навигация, файлы, просмотр журналов, grep, каналы, разрешения, процессы и архивы, с практи…

Читать статью →
Автоматизация тестирования

Selenium vs Playwright: ключевые различия, сравнение кода и что выбрать

Сравнение Selenium и Playwright: архитектура, ожидание, поддержка браузера и языка, отладка, одинаковый тест в обоих инструментах, советы по миграци…

Читать статью →
Тестирование ПО

Разделение эквивалентности: метод проектирования тестов, объясненный на рабочих примерах

Объяснение эквивалентного разделения: как найти действительные и недействительные разделы, рабочие примеры для возрастов, паролей и купонов, проведе…

Читать статью →
Тестирование ПО

Серьезность и приоритет в тестировании программного обеспечения: различия, матрица и реальные примеры

Объяснение серьезности и приоритета при тестировании программного обеспечения: четкие определения, четырехсторонняя матрица с примерами, шкалы уровн…

Читать статью →
Базы данных и SQL

Учебное пособие по подзапросам SQL: типы, коррелирующие подзапросы и EXISTS с примерами

Руководство по подзапросам SQL с выполненными нами запросами: скалярные подзапросы, IN, FROM и SELECT, коррелированные подзапросы, EXISTS, ловушка N…

Читать статью →
Базы данных и SQL

Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

Транзакции SQL и ACID, объясненные с помощью банковского перевода, который мы выполнили: COMMIT, ROLLBACK, точки сохранения, блокировки, уровни изол…

Читать статью →
Разработка ПО

Git merge vs rebase: отличия, примеры и когда что использовать

Git merge vs rebase с реальным выводом log, сравнительной таблицей, золотым правилом rebase и безопасным рабочим процессом для команды.

Читать статью →
Разработка ПО

Генераторы списков в Python: синтаксис, примеры и когда их избегать

Генераторы списков в Python: синтаксис, фильтры, if-else, вложенные циклы, измеренное время и случаи, когда обычный цикл for лучше.

Читать статью →
Базы данных и SQL

SQL JOIN: объяснение с примерами (Inner, Left, Right, Full)

Разберите SQL JOIN шаг за шагом на примерах таблиц и запросов: INNER, LEFT, RIGHT, FULL, CROSS и SELF, а также частые ошибки и советы по производите…

Читать статью →