Что такое генераторы (generators) в Python и как они работают?

Ответ

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

Как работает yield:

  1. При вызове функции-генератора она возвращает объект-генератор, не начиная выполнение.
  2. При первом вызове next() выполнение функции идет до первого yield, возвращает значение и замораживает свое состояние (все локальные переменные сохраняются).
  3. При следующем вызове next() выполнение возобновляется с места после yield и продолжается до следующего yield.
  4. Когда функция завершается (или встречает return), генерируется исключение StopIteration.

Пример функции-генератора:

def countdown(n):
    print("Генератор запущен")
    while n > 0:
        yield n  # Возврат значения и пауза
        n -= 1
    print("Генератор завершен")

# Создание генератора
counter = countdown(3)
print(counter)  # <generator object countdown at 0x...>

# Итерация по генератору
print(next(counter))  # Вывод: "Генератор запущен", затем: 3
print(next(counter))  # 2
print(next(counter))  # 1
# print(next(counter))  # Вызвало бы StopIteration

# Обычно используют в цикле for, который сам обрабатывает StopIteration
for number in countdown(5):
    print(f"Обратный отсчет: {number}")

Генераторные выражения (аналог list comprehension):

# Создает список в памяти сразу
squares_list = [x**2 for x in range(1000000)]  # Много памяти!

# Создает генератор, который вычисляет значения на лету
squares_gen = (x**2 for x in range(1000000))  # Экономия памяти
print(next(squares_gen))  # 0
print(next(squares_gen))  # 1

Ключевые преимущества:

  • Экономия памяти: Идеально для обработки больших файлов, потоков данных или бесконечных последовательностей.
  • Ленивые вычисления: Значения вычисляются только когда они действительно нужны.
  • Сохранение состояния: Генератор "помнит" свое состояние между вызовами.

Практическое применение: Чтение больших файлов построчно.

def read_large_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:  # file объект сам является итератором
            yield line.strip()

# В памяти одновременно находится только одна строка
for line in read_large_file("huge_log.txt"):
    if "ERROR" in line:
        process_error(line)

Ответ 18+ 🔞

А, генераторы в Python, ёпта! Ну это же просто песня, а не фича, если честно. Представь себе, что у тебя есть задача обработать файл размером с твою совесть после вчерашнего — гигабайт на десять. И если ты тупо загрузишь его весь в память списком, то твоя программа накроется медным тазом, а оперативка взвоет, как Муму перед утоплением.

Так вот, генераторы — это такие хитрожопые функции, которые вместо того, чтобы вывалить тебе сразу всю кучу данных, подкидывают их по одной штуке, как заправский карманник. Сказал «дай» — получил значение. Не сказал — они и не парились, ждут.

Секрет их в волшебном слове yield. Это не просто return, который, как последний мудак, отдал всё и свалил. yield — он умный. Он отдал тебе значение, заморозил всю свою внутреннюю кухню (все переменные, счётчики, состояние) и заснул. А ты иди, работай. Как только ты снова позовёшь, он проснётся ровно на том же месте, где уснул, сделает следующий шаг и снова «йелднется». И так до победного, пока не кончится.

Смотри, как это выглядит, блядь:

def countdown(n):
    print("Генератор запущен")
    while n > 0:
        yield n  # Держи значение и стой, сука, на паузе!
        n -= 1
    print("Генератор завершен")

# Вот тут он ещё нихуя не делает. Просто создал объект-обещание.
counter = countdown(3)
print(counter)  # <generator object countdown at 0x...> — вот он, красавец.

# А вот тут начинается магия. Тыкаешь в него next().
print(next(counter))  # Пишет "Генератор запущен" и выдаёт 3. И засыпает.
print(next(counter))  # Просыпается, уменьшает n, выдаёт 2. Спит.
print(next(counter))  # Просыпается, уменьшает, выдаёт 1. Спит.
# print(next(counter))  # А вот тут он проснётся, поймёт, что цикл кончился, и выплюнет StopIteration — типа, всё, пиздец, приехали.

Но нормальные люди, конечно, не тыкают в него next() как в говно пальцем. Его в цикл for пихают, он там сам всё красиво обходит и StopIteration ловит.

А ещё есть генераторные выражения — это как списковые включения, только для ленивых и умных. Сравни:

# Это — тупой быдлокод для мажоров с 64 гигами оперативки. Создаст список из ЛЯМА чисел СРАЗУ.
squares_list = [x**2 for x in range(1000000)]  # Память, блядь, прощай!

# А это — изящно, как кот, сука, собака. Создаст генератор, который будет вычислять квадраты ПО ТРЕБОВАНИЮ.
squares_gen = (x**2 for x in range(1000000))
print(next(squares_gen))  # 0 — только посчитал ноль.
print(next(squares_gen))  # 1 — только посчитал один. Остальные 999998 чисел пока в небытии.

Зачем это всё, спросишь? Да затем, ёпта! Чтобы не обосраться с памятью. Самый классический пример — читать здоровенный лог-файл построчно и искать там ошибки.

def read_large_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:  # Кстати, сам файловый объект — он тоже итератор, умная жопа!
            yield line.strip()

# И вот ты летишь по этому файлу, как угорелый.
# В памяти в каждый момент — максимум одна строка, а не весь файл целиком.
for line in read_large_file("huge_log.txt"):
    if "ERROR" in line:
        process_error(line)  # Нашёл кринж — обработал.

Вот и вся философия. Вместо того чтобы тащить на себе весь воз данных, генераторы подвозят их на тележке по одному кирпичику. Экономия памяти — овердохуищная, а ленивые вычисления — это вообще святое. Красота, блядь, а не инструмент.