Практикум применяет генераторы из урока 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.