💻 Примеры

⚡ Шпаргалка практикума

def custom_range(start, stop, step=1):
    while start < stop:
        yield start
        start += step

print(list(custom_range(2, 10, 2)))  # [2, 4, 6, 8]
ЗадачаИнструмент
Свой range()while + yield, условие строго start < stop
Случайные датыrandom.randint() + словарь дней месяца, is_leap() для февраля
Комбинирование генераторовtuple(next(gen) for gen in generators)
Циклическое распределениеitertools.cycle()
Бесконечный генераторостанавливать через break/return/islice, а не ждать вечно
Топ-3 ошибки: лишнее равенство в условии цикла (start <= stop вместо <) · не учли високосный год для февраля · бесконечное ожидание в системе распределения задач без лимита попыток.

Практикум применяет генераторы из урока 56 к пяти реалистичным задачам: собственный range(), случайные даты и имена, комбинирование нескольких генераторов и файловая система распределения задач. Примеры идут в том же порядке, что source, и добавляют по одному шагу «а что если» к каждой задаче. Весь вывод в комментариях получен запуском кода; для примеров со случайными числами вывод зафиксирован через random.seed(42), чтобы его можно было повторить.

Пример 1. custom_range: собственный генератор вместо range()

Базовый шаблон генератора: обычная функция, но вместо return — yield, и вместо одного значения она отдаёт последовательность, останавливаясь после каждого yield до следующего запроса.

def custom_range(start, stop, step=1):
    while start < stop:
        yield start
        start += step

for num in custom_range(2, 10, 2):
    print(num)
# 2
# 4
# 6
# 8

print(type(custom_range(0, 3)))  # <class 'generator'> — вызов не выполняет тело функции

Что происходит: вызов custom_range(0, 3) не запускает тело функции сразу — он создаёт объект-генератор. Код внутри начинает выполняться, только когда генератор перебирают циклом for или дёргают next(), и останавливается на каждом yield, запоминая место.

Пример 2. Ловушка границы: < против <=

Ошибка 1 из раздела «Ошибки» этого урока — лишний знак равенства в условии цикла. Разница в один символ меняет, включена ли верхняя граница диапазона.

def custom_range_bad(start, stop, step=1):
    while start <= stop:  # лишнее равенство
        yield start
        start += step

print("bad:", list(custom_range_bad(1, 5)))   # [1, 2, 3, 4, 5] — включает 5
print("good:", list(custom_range(1, 5)))       # [1, 2, 3, 4]    — не включает 5
print("range():", list(range(1, 5)))            # [1, 2, 3, 4]    — эталон поведения

Как проверить себя: встроенный range(1, 5) — эталон, с которым нужно сверяться: верхняя граница в Python традиционно не включается (полуоткрытый интервал). Если ваш custom_range расходится с range() на границах — ищите <= там, где должно быть <.

Пример 3. Случайные даты: словарь дней месяца и високосный год

Генератор случайных дат должен знать, сколько дней в каждом месяце — а для февраля это число зависит от того, високосный ли год. Ошибка 2 раздела «Ошибки» — использовать для февраля всегда 28, забыв про is_leap().

import random
random.seed(42)  # для воспроизводимого вывода в примере

def is_leap(year: int) -> bool:
    return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)

def random_dates(year: int):
    days_in_month = {
        1: 31, 2: 29 if is_leap(year) else 28, 3: 31,
        4: 30, 5: 31, 6: 30, 7: 31, 8: 31,
        9: 30, 10: 31, 11: 30, 12: 31
    }
    while True:
        month = random.randint(1, 12)
        day = random.randint(1, days_in_month[month])
        yield f"{year}-{month:02d}-{day:02d}"

print(is_leap(2024), is_leap(2023), is_leap(1900), is_leap(2000))
# True False False True — 1900 делится на 100, но не на 400: не високосный

date_gen = random_dates(2025)
for _ in range(5):
    print(next(date_gen))
# 2025-11-04
# 2025-01-24
# 2025-05-08
# 2025-04-05
# 2025-12-04

Почему так: правило високосного года — не «делится на 4», а «делится на 4, и если делится на 100, то должно делиться и на 400». Отсюда пары в примере: 1900 делится на 4 и на 100, но не на 400 — не високосный; 2000 делится на все три — високосный. random_dates — бесконечный генератор (while True): без ограничения range(5) в цикле вызова он не остановится сам.

Пример 4. Случайные имена: комбинация двух списков через random.choice

from random import choice

def random_names():
    first_names = ["Alice", "Bob", "Charlie", "David", "Emma"]
    last_names = ["Smith", "Johnson", "Brown", "Williams", "Jones"]
    while True:
        yield f"{choice(first_names)} {choice(last_names)}"

name_gen = random_names()
for _ in range(5):
    print(next(name_gen))
# Emma Smith
# Emma Williams
# Alice Smith
# Alice Johnson
# Bob Jones

Что происходит: choice() — не randint(): он берёт случайный элемент из последовательности, а не случайное число. Имя и фамилия выбираются независимо, поэтому повторы имени (Emma дважды подряд) — не ошибка, а ожидаемое поведение случайного выбора.

Пример 5. combine_generators: объединить несколько генераторов в один поток

Задача 4 — построить генератор тестовых данных, комбинируя уже готовые random_names и random_dates. Решение — функция с произвольным числом генераторов через *args.

def combine_generators(*generators):
    while True:
        yield tuple(next(gen) for gen in generators)

name_gen2 = random_names()
date_gen2 = random_dates(2025)
data_gen = combine_generators(name_gen2, date_gen2)
for _ in range(5):
    print(next(data_gen))
# ('Emma Smith', '2025-09-07')
# ('Emma Williams', '2025-04-15')
# ('Emma Brown', '2025-01-25')
# ('Bob Williams', '2025-06-09')
# ('Bob Johnson', '2025-06-04')

Что важно: *generators принимает любое количество генераторов, а генераторное выражение next(gen) for gen in generators — знакомая тема из урока 55 — забирает по одному значению из каждого за один проход. Если один из генераторов исчерпается раньше других, next() внутри выражения бросит StopIteration — с бесконечными random_names/random_dates это не случится, но с конечным генератором (как custom_range) — вполне может.

Пример 6. Циклическое распределение через itertools.cycle

Сердце системы распределения задач (задача 5) — не файлы сами по себе, а идея «раздать по кругу»: третий сотрудник получает четвёртую задачу, потому что цикл сотрудников начинается заново.

import itertools

employees = ["Alice", "Bob", "Charlie"]
tasks = ["Отчёт", "Письмо", "Презентация", "Звонок", "Обзор кода"]

employee_cycle = itertools.cycle(employees)
for task in tasks:
    employee = next(employee_cycle)
    print(f"{employee} выполняет: {task}")
# Alice выполняет: Отчёт
# Bob выполняет: Письмо
# Charlie выполняет: Презентация
# Alice выполняет: Звонок     — цикл начался заново
# Bob выполняет: Обзор кода

Почему так: itertools.cycle(employees) — тоже бесконечный генератор, только он не считает и не выбирает случайно, а просто повторяет исходную последовательность по кругу бесконечно. Пяти задач на трёх сотрудников хватает, чтобы увидеть цикл: четвёртая задача снова досталась Alice.

Пример 7. Запись задач в файл: add_tasks()

Первая половина системы задачи 5 — программа, которая копит задачи в файле, пока пользователь не введёт exit. Чтобы запустить её без реального терминала, ввод пользователя здесь подменён заранее заготовленным списком ответов.

def add_tasks():
    while True:
        task = input("Введите задачу (или 'exit' для выхода): ").strip()
        if task.lower() == "exit":
            break
        with open("tasks.txt", "a", encoding="utf-8") as file:
            file.write(task + "\n")
        print("Задача добавлена!")

add_tasks()
# при вводе "Отчёт", "Письмо", "exit" — в консоли:
# Задача добавлена!
# Задача добавлена!

with open("tasks.txt", "r", encoding="utf-8") as file:
    print(file.read())
# Отчёт
# Письмо

Что происходит: файл открывается заново на каждой добавленной задаче ("a" — дозапись), а не один раз в начале — так задача сохраняется на диск сразу, а не теряется, если программу закроют посреди работы.

Пример 8. Чтение и распределение задач: task_assigner()

Вторая половина системы — генератор, который построчно читает файл и раздаёт прочитанные задачи сотрудникам по кругу (пример 6), пока файл не закончится.

def task_assigner(employees, filename="tasks.txt"):
    employee_cycle = itertools.cycle(employees)
    attempts = 0
    retries = 5

    while attempts < retries:
        try:
            with open(filename, "r", encoding="utf-8") as file:
                while True:
                    task = file.readline().strip()
                    if task:
                        yield next(employee_cycle), task
                    else:
                        return  # для этого примера: выходим, а не ждём новых строк
        except FileNotFoundError:
            attempts += 1
            print(f"Файл не найден, попытка {attempts}/{retries}...")

employees = ["Alice", "Bob", "Charlie"]
for employee, task in itertools.islice(task_assigner(employees), 5):
    print(f"{employee} выполняет: {task}")
# Alice выполняет: Отчёт
# Bob выполняет: Письмо
# (запросили 5 — получили только 2: в файле tasks.txt всего 2 строки)

Отличие от эталонного решения: в «Решениях» (решение 5) вместо return при пустой строке стоит time.sleep(3) внутри того же while True — программа ждёт, что вторая, параллельно запущенная программа (add_tasks.py) допишет файл. Такой вариант нельзя безопасно запустить и получить вывод в статичном примере — он никогда не завершится сам, поэтому здесь показан бегущий, но эквивалентный по логике вариант, который останавливается сам, когда строки закончились.

Пример 9. Почему у бесконечного генератора обязательно должен быть внешний предел

Ошибка 3 раздела «Ошибки» — генератор или цикл без выхода. random_dates, random_names, itertools.cycle и незавершённый task_assigner — все они технически бесконечны. Их безопасно использует не сам генератор, а то, как его вызывают.

def counting_forever():
    n = 0
    while True:
        n += 1
        yield n

infinite = counting_forever()
first_five = [next(infinite) for _ in range(5)]
print(first_five)  # [1, 2, 3, 4, 5] — ограничили range(5), а не сам генератор

print(list(itertools.islice(counting_forever(), 3)))  # [1, 2, 3]
Правило: предел ставит не тело генератора (там осознанно while True), а вызывающий код — for _ in range(n), itertools.islice(gen, n) или явный break/return по условию, как в примере 8. Забыть об этом на вызывающей стороне — и получите зависшую программу, даже если сам генератор написан правильно.

Что дальше: следующий урок, урок 58, заменит самодельные строки дат вида f"{year}-{month:02d}-{day:02d}" из примера 3 на модуль datetime — там же появятся готовые способы проверки дат и работы с календарём без ручного словаря days_in_month.