Этот урок закрепляет три темы блока: файлы (урок 52), генераторы (урок 56) и JSON и datetime (урок 58). Примеры идут от простого к сложному: сначала каждая тема по отдельности, затем связки «генератор + файл» и «JSON + datetime», и в конце — капстоун-пример, объединяющий все три. Весь вывод в комментариях получен реальным запуском кода, а не набран по памяти.
Пример 1. Генератор с yield
Генераторная функция выглядит как обычная, но вместо return использует yield и возвращает не значение, а объект-генератор — значения из него достаются по одному в цикле for.
# generator_01_basic.py
def count_up_to(n: int):
count = 1
while count <= n:
yield count
count += 1
gen = count_up_to(5)
print(type(gen))
for number in gen:
print(number)
# генератор одноразовый: второй проход ничего не даст
print(list(gen))
# <class 'generator'>
# 1
# 2
# 3
# 4
# 5
# []
Что происходит: count_up_to(5) не выполняет тело функции сразу — она лишь создаёт генератор. Тело запускается по кусочкам при каждом next() внутри for, до очередного yield. После того как значения выданы один раз, генератор исчерпан — второй проход list(gen) возвращает пустой список.
Пример 2. Бесконечный генератор и явная остановка
Генератор с while True никогда не закончится сам — выход из цикла обязан сделать вызывающий код, например через break. Это полезно для потоков данных, у которых нет заранее известного конца.
# generator_02_infinite.py
def infinite_counter(start: int = 1):
count = start
while True:
yield count
count += 1
gen = infinite_counter()
for number in gen:
if number > 5:
break
print(number)
# 1
# 2
# 3
# 4
# 5
Что происходит: без break цикл работал бы вечно и программа зависла бы. Условие if number > 5 проверяется на каждой итерации до печати — как только оно срабатывает, цикл прерывается, а генератор просто перестаёт запрашивать новые значения.
Пример 3. yield from — делегирование другому генератору
yield from iterable выдаёт все значения вложенного генератора или любой другой последовательности, как будто они выдаются напрямую — без ручного вложенного цикла for ... yield.
# generator_03_yield_from.py
def process_values(data: list[int]):
for value in data:
yield value * 2
def main_generator(*sequences: list[int]):
for seq in sequences:
yield from process_values(seq)
for result in main_generator([1, 2, 3], [10, 15]):
print(result)
# 2
# 4
# 6
# 20
# 30
Что происходит: main_generator перебирает переданные списки и для каждого делегирует выдачу значений process_values через yield from. Без делегирования пришлось бы писать for value in process_values(seq): yield value — yield from делает то же самое короче.
Пример 4. Принудительное закрытие генератора
Метод close() досрочно завершает генератор, когда дальнейшие значения не нужны — например, нужное найдено и продолжать перебор бессмысленно.
# generator_04_close.py
def sensor_data(data: list[int]):
for value in data:
yield value
numbers = [10, 20, 30, 40, 50]
gen = sensor_data(numbers)
for element in gen:
print("Получено значение:", element)
if element >= 30:
print("Значение найдено, закрываем генератор.")
gen.close()
break
try:
next(gen)
except StopIteration as error:
print("StopIteration после close():", repr(error))
# Получено значение: 10
# Получено значение: 20
# Получено значение: 30
# Значение найдено, закрываем генератор.
# StopIteration после close(): StopIteration()
Что происходит: gen.close() останавливает генератор на текущем yield, поднимая внутри него GeneratorExit. Любой следующий вызов next() для закрытого генератора завершается StopIteration — так же, как если бы генератор просто исчерпал все значения сам.
Пример 5. Почему генератор экономит память
Ключевое преимущество генератора над списком — он не хранит все элементы сразу. Проверим это напрямую через размер объекта в памяти.
sys.getsizeof — дополнение курса, в материалах урока 56 не рассматривалось. Смотрите документацию по sys.getsizeof.
# generator_05_memory.py
import sys
million = range(1_000_000)
as_list = [x * x for x in million]
as_generator = (x * x for x in million)
print("list:", sys.getsizeof(as_list), "байт")
print("generator:", sys.getsizeof(as_generator), "байт")
total_list = sum(as_list)
total_gen = sum(as_generator)
print("Суммы совпадают:", total_list == total_gen, total_list)
# list: 8448728 байт
# generator: 208 байт
# Суммы совпадают: True 333332833333500000
Что происходит: список сразу считает и хранит все 1 000 000 квадратов — почти 8.5 МБ. Генератор хранит только состояние вычисления — 208 байт независимо от того, сколько элементов он выдаст. Суммы при этом равны: генератор вычисляет те же значения, просто по одному и по требованию.
Пример 6. Файлы: режимы, кодировка и with
Из урока 52: контекстный менеджер with open(...) гарантированно закрывает файл, а режим (r, w, a) и encoding="utf-8" определяют, как файл открывается и читается.
# file_01_read_write.py
lines = ["Первая строка\n", "Вторая строка\n", "Третья строка\n"]
with open("demo.txt", "w", encoding="utf-8") as file:
file.writelines(lines)
with open("demo.txt", "r", encoding="utf-8") as file:
content = file.read()
print(repr(content))
with open("demo.txt", "r", encoding="utf-8") as file:
for line in file:
print(line.strip().upper())
with open("demo.txt", "a", encoding="utf-8") as file:
file.write("Добавленная строка\n")
with open("demo.txt", "r", encoding="utf-8") as file:
print(file.readlines())
# 'Первая строка\nВторая строка\nТретья строка\n'
# ПЕРВАЯ СТРОКА
# ВТОРАЯ СТРОКА
# ТРЕТЬЯ СТРОКА
# ['Первая строка\n', 'Вторая строка\n', 'Третья строка\n', 'Добавленная строка\n']
Что происходит: режим "w" создаёт файл заново (перезаписывает), "r" — читает, "a" — дописывает в конец, не удаляя старое содержимое. Цикл for line in file построчно перебирает файл, а каждый блок with закрывает файл сам, даже если внутри произошла ошибка.
Пример 7. Генератор для ленивого чтения файла
Связка тем: генератор строк файла не грузит весь файл в память и по пути отфильтровывает пустые строки — так рекомендует делать с большими файлами урок 56.
# file_02_lazy_reading_generator.py
def read_non_empty_lines(path: str):
with open(path, "r", encoding="utf-8") as file:
for line in file:
stripped = line.strip()
if stripped:
yield stripped
with open("log.txt", "w", encoding="utf-8") as file:
file.write("Строка 1\n\nСтрока 2\n \nСтрока 3\n")
for entry in read_non_empty_lines("log.txt"):
print(entry)
# Строка 1
# Строка 2
# Строка 3
Что происходит: read_non_empty_lines открывает файл и построчно выдаёт непустые строки через yield, ни разу не сохраняя весь файл целиком. Пустая строка и строка из одних пробелов после .strip() становятся пустой строкой "" и отсеиваются условием if stripped. Файл автоматически закрывается, когда генератор исчерпан или прерван — за это отвечает with внутри тела генератора.
Пример 8. JSON: сериализация, десериализация и форматирование
Из урока 58: json.dumps/json.loads работают со строками, параметры indent, ensure_ascii и sort_keys управляют читаемостью результата.
# json_01_basic.py
import json
person = {"name": "Alice", "age": 25, "is_student": False, "pet": None}
json_string = json.dumps(person, ensure_ascii=False, indent=2)
print(json_string)
loaded = json.loads(json_string)
print(loaded["name"], type(loaded))
city = {"country": "Беларусь", "city": "Минск"}
print(json.dumps(city))
print(json.dumps(city, ensure_ascii=False))
print(json.dumps(city, ensure_ascii=False, sort_keys=True))
# {
# "name": "Alice",
# "age": 25,
# "is_student": false,
# "pet": null
# }
# Alice <class 'dict'>
# {"country": "\u0411\u0435\u043b\u0430\u0440\u0443\u0441\u044c", "city": "\u041c\u0438\u043d\u0441\u043a"}
# {"country": "Беларусь", "city": "Минск"}
# {"city": "Минск", "country": "Беларусь"}
Что происходит: False/None становятся false/null — JSON использует свои обозначения, не Python-овские. Без ensure_ascii=False кириллица экранируется в \uXXXX. sort_keys=True меняет порядок ключей на алфавитный — сравните последние две строки: city встал перед country, хотя в словаре был вторым.
Пример 9. JSON: файл и обработка JSONDecodeError
json.dump/json.load работают с файловым объектом напрямую — записывать промежуточную строку не нужно. Некорректный JSON стоит перехватывать явно, как учит урок 58.
# json_02_file_and_errors.py
import json
data = {"tasks": ["Купить билеты", "Сдать отчёт"], "count": 2}
with open("data.json", "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=2)
with open("data.json", "r", encoding="utf-8") as file:
restored = json.load(file)
print(restored)
broken_json = "{'x': 1}"
try:
json.loads(broken_json)
except json.JSONDecodeError as error:
print("JSONDecodeError:", error)
# {'tasks': ['Купить билеты', 'Сдать отчёт'], 'count': 2}
# JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
Что происходит: json.dump(data, file, ...) сразу пишет в открытый файл, а json.load(file) читает и разбирает его обратно в словарь. Строка "{'x': 1}" с одинарными кавычками — не валидный JSON (нужны двойные), поэтому json.loads поднимает JSONDecodeError с точным указанием, где разбор сломался.
Пример 10. datetime: текущее время, форматы и разница дат
strftime превращает datetime в строку по шаблону, strptime — строку обратно в datetime, а вычитание двух дат даёт timedelta.
# datetime_01_basic.py
from datetime import datetime, timedelta
now = datetime.now()
print(type(now))
print(now.strftime("%d.%m.%Y %H:%M:%S"))
deadline = datetime.strptime("01.12.2026", "%d.%m.%Y")
days_left = (deadline - now).days
print(f"До дедлайна осталось дней: {days_left}")
print(deadline > now)
future = now + timedelta(weeks=2)
print(future.strftime("%Y-%m-%d"))
# <class 'datetime.datetime'>
# 13.08.2026 21:01:02 (у вас будет другое время — используется текущий момент)
# До дедлайна осталось дней: 109 (у вас будет другое число — зависит от сегодняшней даты)
# True
# 2026-08-27 (у вас будет другая дата — это "через 2 недели от сегодня")
Что происходит: datetime.strptime("01.12.2026", "%d.%m.%Y") обязан получить строку в точности в формате %d.%m.%Y, иначе поднимется ValueError. Разница deadline - now — это объект timedelta, у которого есть .days. Объекты datetime сравниваются напрямую операторами >/<, как числа. Все значения, вычисленные от datetime.now() (время, число дней до дедлайна, дата через две недели), — плывущие: они верны только на момент запуска, и на другой день дадут другие числа.
Пример 11. Капстоун: просроченные задачи из JSON через генератор
Собираем все три темы блока в одной задаче — так же, как в практической работе «Поиск просроченных задач»: JSON хранит задачи, генератор лениво отбирает нужные, datetime сравнивает сроки.
# capstone_overdue_tasks.py
import json
from datetime import datetime
tasks_json = """
[
{"title": "Audit finances", "deadline": "23-12-2025", "status": "done"},
{"title": "Plan event", "deadline": "15-09-2025", "status": "pending"},
{"title": "Team meeting", "deadline": "15-04-2025", "status": "pending"},
{"title": "Backup server", "deadline": "19-03-2025", "status": "pending"},
{"title": "Sign contract", "deadline": "28-07-2025", "status": "pending"}
]
"""
def load_tasks(raw_json: str):
return json.loads(raw_json)
def overdue_tasks(tasks: list[dict], check_date: datetime):
for task in tasks:
if task["status"] != "pending":
continue
deadline = datetime.strptime(task["deadline"], "%d-%m-%Y")
if deadline < check_date:
yield task
tasks = load_tasks(tasks_json)
check_date = datetime.strptime("01-08-2025", "%d-%m-%Y")
for task in overdue_tasks(tasks, check_date):
print(f"- {task['title']} ({task['deadline']})")
# - Team meeting (15-04-2025)
# - Backup server (19-03-2025)
# - Sign contract (28-07-2025)
Что происходит: load_tasks превращает JSON-строку в список словарей через json.loads. overdue_tasks — генератор: он не строит список заранее, а по одной задаче проверяет статус pending и сравнивает распарсенный через strptime срок с датой проверки, выдавая (yield) только просроченные. «Audit finances» и «Plan event» отсеялись — первая уже done, у второй срок ещё не наступил.
Что делать дальше
Если что-то из примеров не запустилось — загляните в типичные ошибки, затем переходите к заданиям, включая практику «Поиск просроченных задач».