Задумка и устройство программы: ТЗ, функции, свой модуль, __name__ и assert

Вспомните: в главе 3 утилита расходов получила параметры запуска, окно с выбором папки, настройки в JSON и стала exe. Каждый раз программа была одним файлом, который рос сверху вниз. Для итогового проекта так не пойдёт: вы будете делать свою утилиту, и чтобы её можно было доделать, проверить и показать, её надо сначала придумать и разложить по полочкам. Этим и займёмся.

🎯 После урока сможете: выбрать задачу для итогового проекта по трём признакам, описать её в ТЗ на одну страницу (вход, выход, ошибки, интерфейс), разбить программу на функции «ввод — обработка — вывод» так, чтобы каждая делала одну работу, вынести эти функции в свой модуль utils.py и подключить его через import и from … import, защитить код запуска строкой if __name__ == "__main__": и проверить функции на маленьких данных через assert — ещё до окна.

Сквозной пример урока — «Разборщик чеков»: папка с выгрузками трат из банка в CSV превращается в итоги по категориям и отчёт JSON. Это образец, а не обязательная тема: проект вы делаете свой.

Какую задачу взять

Итоговый проект проваливается чаще всего не на коде, а на выборе задачи: «сделаю помощника для всего» или «что-нибудь с нейросетями». Хорошая задача проходит три проверки:

  1. Своя рутина. Вы сами делаете это руками и регулярно: раскладываете файлы, сводите таблицы, ищете в журналах. Тогда вы знаете, что считать правильным результатом, и программа пригодится после курса.
  2. Есть данные. Файлы для работы у вас уже есть или их легко получить: выгрузки, папка с фото, журнал. Нет данных — не на чем проверять, проект превращается в «окно с кнопками, которые ничего не делают».
  3. Результат проверяем. Можно взять маленький кусок данных, посчитать ответ вручную и сравнить с программой. «Подобрать лучший вариант» или «предсказать» так не проверишь.

Ещё одно ограничение — объём: на проект у вас около двух недель. Задача должна укладываться в инструменты курса: файлы, CSV и JSON, даты, регулярные выражения, командная строка, окно Tkinter и exe. Интернет, базы данных и нейросети — это другие курсы.

Запишите три-четыре своих рутины и прогоните каждую через три признака. Оставьте ту, где на все три ответ «да» и где ручная работа раздражает сильнее всего: доводить такую задачу до конца хочется самому.

Проверьте себя

Контрольный вопрос. Какая задумка проходит все три признака хорошего итогового проекта?

АПрограмма, которая сама подберёт, куда выгоднее вложить деньги
БУтилита, которая раз в месяц складывает выгрузки трат из банка по категориям
ВОкно с красивыми кнопками, а что они будут делать, решу по ходу
ГПомощник для всех задач на компьютере сразу

Подсказка: Пройдите по каждому варианту тремя вопросами: делаю ли я это руками сам, есть ли файлы, можно ли сверить ответ с подсчётом на бумаге?

Онлайн-проверка ответа появится позже

ТЗ на одну страницу

ТЗ (техническое задание) — короткий текст, где задача описана до кода. Одна страница, не больше: длинное ТЗ никто не дочитает, включая вас. В нём четыре обязательные части и строка о готовности:

  • Вход — что программа получает: какие файлы, в какой кодировке, какие колонки, какие параметры.
  • Выход — что и куда выдаёт: файл какого формата, текст на экране, в каком виде числа.
  • Ошибки — что делать с плохими данными: нет файла, пустая папка, кривая строка. Здесь пригодится всё из главы 2.
  • Интерфейс — как человек запускает программу: командная строка с параметрами или окно, какие в нём кнопки.
  • Готово, когда — на каких данных и с каким результатом вы проверите, что программа работает.

ТЗ «Разборщика чеков» выглядит так:

Разборщик чеков — ТЗ

Зачем: раз в месяц трачу час, чтобы сложить траты из выгрузок
банка по категориям. Хочу за минуту.

Вход: папка с CSV-выгрузками (UTF-8, разделитель «;»,
колонки дата;категория;сумма, сумма может быть «1 240,50»).

Выход: файл otchet.json — категория -> сумма за все файлы,
плюс те же итоги на экране, по две цифры после точки.

Ошибки: папки нет или в ней нет CSV — сообщение, без падения;
строка с плохой суммой пропускается и считается;
«Кафе» и «кафе » — одна категория.

Интерфейс: окно — кнопка «Выбрать папку», кнопка «Посчитать»,
список итогов; выбранная папка запоминается в settings.json.

Готово, когда: на двух моих выгрузках за сентябрь итоги
совпадают с подсчётом вручную.
Лист бумаги формата А4 с заголовком «ТЗ: Разборщик чеков». Лист разделён на пять цветных полос с крупными подписями слева: «Вход» (синяя), «Выход» (зелёная), «Ошибки» (красная), «Интерфейс» (фиолетовая), «Готово, когда» (серая). В каждой полосе одна-две короткие строки из примера: папка с CSV-выгрузками; otchet.json и итоги на экране; нет папки — сообщение, плохая строка пропускается; окно с двумя кнопками и списком; итоги совпадают с подсчётом вручную.

Заметьте: в ТЗ нет ни слова про функции и модули — только что программа делает для человека. Как она устроена внутри, решаем следующим шагом.

Проверьте себя

Контрольный вопрос. Коллега прислал ТЗ своей утилиты. Какой обязательной части в нём нет?

Переименователь фото — ТЗ

Вход: папка с фотографиями .jpg, имена вида IMG_4512.jpg.

Выход: те же файлы, переименованные в дату съёмки:

2026-09-14_001.jpg, 2026-09-14_002.jpg и так далее.

Интерфейс: командная строка, один параметр — путь к папке:

python foto.py D:\Фото\Сентябрь

Готово, когда: на папке из 20 фото все имена стали датами.

АЧто программа получает на вход
БЧто программа выдаёт на выходе
ВЧто делать с плохими данными и сбоями
ГКак человек запускает программу

Подсказка: Сверьте заголовки ТЗ со списком обязательных частей из урока. Какой заголовок не встретился ни разу?

Онлайн-проверка ответа появится позже

Контрольный вопрос. В этом ТЗ все заголовки на месте, но по одной части программу всё равно не написать — непонятно, что делать. Чего не хватает?

Поиск в журнале — ТЗ

Вход: текстовый журнал программы (UTF-8), до 2 ГБ.

Выход: показывает результат.

Ошибки: файла нет — сообщение; строки в другой кодировке

пропускаются, их число выводится в конце.

Интерфейс: командная строка: путь к журналу и слово для поиска.

АКакой файл программа читает и в какой кодировке
БВ каком виде и куда выдаётся результат поиска
ВЧто делать, если журнала нет
ГКак программа получает слово для поиска

Подсказка: Прочитайте каждую часть и спросите себя: смогу ли я по этой строке написать код, не задав автору ни одного вопроса?

Онлайн-проверка ответа появится позже

Ввод — обработка — вывод

Почти любая утилита из курса делает три вещи: ввод — достаёт данные из файлов или параметров, обработка — считает, сортирует, ищет, вывод — пишет результат в файл или показывает человеку. Разложите программу на функции ровно по этим трём слоям. Тогда окно, командная строка и exe из главы 3 будут лишь по-разному вызывать одни и те же функции обработки.

Схема из трёх блоков слева направо, соединённых стрелками. Первый блок «Ввод»: значок папки с файлами CSV и функция read_rows. Второй блок «Обработка»: функции parse_amount и totals_by_category, внутри — словарь «категория → сумма». Третий блок «Вывод»: функция save_report и значок файла otchet.json. Над схемой две рамки «окно Tkinter» и «командная строка», от каждой стрелка вниз к схеме с подписью «только вызывают».

У «Разборщика чеков» четыре функции:

import csv
import json


def parse_amount(text):
    """Строка суммы из выгрузки -> число: '1 200,50' -> 1200.5."""
    return float(text.replace(" ", "").replace(",", "."))


def read_rows(path):
    """Ввод: строки одного CSV-файла выгрузки."""
    with open(path, encoding="utf-8", newline="") as f:
        return list(csv.DictReader(f, delimiter=";"))


def totals_by_category(rows):
    """Обработка: сумма трат по каждой категории."""
    totals = {}
    for row in rows:
        category = row["категория"].strip().lower()
        totals[category] = totals.get(category, 0) + parse_amount(row["сумма"])
    return totals


def save_report(totals, path):
    """Вывод: отчёт в JSON."""
    with open(path, "w", encoding="utf-8") as f:
        json.dump(totals, f, ensure_ascii=False, indent=2)
  • read_rows(path) — ввод: открывает один CSV и возвращает список словарей (как в уроке 1.3);
  • parse_amount(text) — обработка одной суммы: убирает пробелы-разделители и меняет запятую на точку;
  • totals_by_category(rows) — обработка: складывает суммы по категориям, приводя имя категории к одному виду через strip().lower();
  • save_report(totals, path) — вывод: записывает словарь итогов в JSON.

Одна функция — одна работа

Правило простое: функцию можно назвать одним глаголом без «и». read_rows читает, save_report сохраняет. Если в названии напрашивается «и» — «прочитай и посчитай», «посчитай и покажи» — функция делает две работы, и её стоит разрезать. Причины практические:

  • функцию обработки можно проверить на списке из трёх строк, без файлов и окна;
  • ту же обработку вызовут и окно, и командная строка — копировать код не придётся;
  • сломалось — сразу видно, в каком слое искать.

Второй признак беды — две функции, которые делают одно и то же под разными именами. Так бывает, когда код копируют из старых программ. Оставьте одну и вызывайте её везде.

Проверьте себя

Контрольный вопрос. Какая функция делает две работы сразу и её стоит разрезать?

Исходный код для этого задания:

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        return f.read().splitlines()


def read_and_count(path):
    with open(path, encoding="utf-8") as f:
        lines = f.read().splitlines()
    return len([line for line in lines if "ошибка" in line])


def save_text(text, path):
    with open(path, "w", encoding="utf-8") as f:
        f.write(text)
Аread_lines
Бread_and_count
Вsave_text

Подсказка: Попробуйте описать, что делает каждая функция, одним глаголом без «и».

Онлайн-проверка ответа появится позже

Контрольный вопрос. В модуле одна функция лишняя: её работу уже делает другая. Какую функцию удалить?

Исходный код для этого задания:

def parse_amount(text):
    return float(text.replace(",", "."))


def to_number(s):
    return float(s.replace(",", "."))


def total(amounts):
    return sum(parse_amount(a) for a in amounts)


def report(value):
    return f"Итого: {value:.2f}"
Аparse_amount
Бto_number
Вtotal
Гreport

Подсказка: Найдите две функции с одинаковым телом. Из этой пары удалите ту, которую не вызывает ни одна функция модуля.

Онлайн-проверка ответа появится позже

Свой модуль: utils.py и import

Модуль — это обычный файл .py. Положите функции ввода, обработки и вывода в файл utils.py, а запуск программы — в main.py рядом, в той же папке. Python ищет модули сначала в папке запускаемого файла, поэтому main.py найдёт utils.py без настроек.

Подключить модуль можно двумя способами:

import utils

print(utils.parse_amount("1 200,50"))      # имя модуля, точка, имя функции

from utils import parse_amount

print(parse_amount("1 200,50"))            # функция доступна по своему имени
1200.5
1200.5
  • import utils — подключает модуль целиком, обращение через точку: utils.read_rows(...). Сразу видно, откуда функция;
  • from utils import read_rows, save_report — берёт из модуля нужные имена, дальше они пишутся без приставки. Удобно, когда функций две-три;
  • при первом import Python выполняет файл модуля целиком, сверху вниз: создаются его функции и переменные, и срабатывают все print, которые стоят вне функций. Повторный import того же модуля файл заново не выполняет;
  • имя модуля в import пишется без .py: import utils, не import utils.py;
  • не называйте свой файл как модуль Python: csv.py или json.py рядом с программой перекроют настоящие, и import csv подключит ваш файл.

Вот main.py «Разборщика чеков». В нём нет ни одной строки обработки — только вызовы:

from pathlib import Path

from utils import read_rows, save_report, totals_by_category


def main():
    rows = []
    for path in sorted(Path("vygruzki").glob("*.csv")):
        rows.extend(read_rows(path))
    totals = totals_by_category(rows)
    save_report(totals, "otchet.json")
    for category, total in sorted(totals.items()):
        print(f"{category}: {total:.2f}")


if __name__ == "__main__":
    main()
Окно проводника: папка проекта, в ней файлы main.py, utils.py и папка vygruzki с двумя CSV. Справа два листа кода рядом. Левый лист main.py: строка from utils import read_rows, save_report, totals_by_category выделена, от неё стрелки ведут к трём заголовкам функций на правом листе utils.py. Над стрелками подпись «берём готовые функции».

Проверьте себя

Задание. Программа из двух файлов лежит в одной папке. Её запускают командой python main.py. Что она напечатает? Введите вывод целиком.

Исходный код для этого задания:

# ===== файл prices.py =====
def with_fee(amount):
    return amount + amount * 3 // 100


def rub(amount):
    return f"{amount} руб."

# ===== файл main.py =====
import prices

total = 0
for a in [500, 1200, 300]:
    total += prices.with_fee(a)
print(prices.rub(total))

Подсказка: Посчитайте with_fee для каждой суммы отдельно: что даёт целочисленное деление на 100? Потом сложите и посмотрите, как rub оформляет число.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Программу запускают командой python main.py. Что выведет программа первой строкой? Введите строку целиком.

Исходный код для этого задания:

# ===== файл rules.py =====
print("загружаю правила")

LIMIT = 1000


def is_big(amount):
    return amount > LIMIT

# ===== файл main.py =====
from rules import is_big

print("старт")
print(sum(1 for a in [300, 1500, 999, 2400] if is_big(a)))

Подсказка: Что делает Python, когда встречает строку с import или from … import: только запоминает имена или выполняет файл модуля сверху вниз?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Ошибки импорта

Две ошибки встречаются в каждом втором первом проекте.

Неверное имя. Python нашёл модуль, но в нём нет функции с таким именем — опечатка или функцию переименовали в одном файле и забыли во втором. Программа останавливается на строке from … import с ошибкой ImportError, а в тексте ошибки названо имя, которого не нашлось. Если опечатка в имени самого модуля, ошибка будет ModuleNotFoundError.

Круговой импорт. main.py импортирует utils.py, а utils.py в ответ импортирует что-то из main.py. Что происходит при python main.py: первая строка main.py отправляет Python выполнять utils.py. Первая строка utils.py просит main — и Python выполняет main.py ещё раз, уже как обычный модуль. Там первая же строка просит функцию из utils.py, а он выполнен только до своей первой строки: функции в нём ещё нет. Итог — ImportError. Каждый файл ждёт другой, и порядок строк внутри файлов это не лечит. Новые версии Python в тексте такой ошибки могут посоветовать переименовать файл — на случай, если он совпал по имени с чужим модулем. При круговом импорте дело не в имени: переименованный файл попадёт в тот же круг. Правило против этого: модуль ничего не берёт из main.py. Всё, что функции нужно, она получает параметрами, а настройки вроде лимитов живут в самом модуле или передаются из main.py при вызове.

# плохо: utils.py берёт LIMIT из main.py
from main import LIMIT

def is_big(amount):
    return amount > LIMIT

# хорошо: лимит приходит параметром
def is_big(amount, limit):
    return amount > limit

Проверьте себя

Контрольный вопрос. Программу запускают командой python main.py, и она сразу останавливается с ошибкой ImportError. В чём причина?

Исходный код для этого задания:

# ===== файл utils.py =====
def parse_amount(text):
    return float(text.replace(",", "."))

# ===== файл main.py =====
from utils import parse_amont

print(parse_amont("12,5"))
АВ utils.py нет функции с именем из строки импорта: в main.py опечатка
БФайл utils.py должен лежать в папке, где установлен Python
ВЧерез from … import нельзя подключать функции, только целые модули
ГПеред именем модуля нужно указать расширение: utils.py

Подсказка: Сравните буква в букву имя после import в main.py и имя после def в модуле.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Оба файла лежат в одной папке, имена написаны без опечаток, но python main.py падает с ImportError. Как исправить устройство программы?

Исходный код для этого задания:

# ===== файл utils.py =====
from main import LIMIT


def is_big(amount):
    return amount > LIMIT

# ===== файл main.py =====
from utils import is_big

LIMIT = 1000

print(is_big(1500))
АУбрать импорт из main.py в utils.py и передавать лимит в is_big параметром
БПоменять местами строки LIMIT = 1000 и from utils import is_big в main.py
ВЗаменить в main.py from utils import is_big на import utils
ГПереименовать utils.py, как советует текст ошибки в новых версиях Python

Подсказка: Пройдите по стрелкам импорта: какой файл Python начинает выполнять, куда уходит и какое имя ещё не успело появиться? Изменится ли этот круг, если поменять имя файла?

Онлайн-проверка ответа появится позже

Запуск или импорт: if __name__ == "__main__"

Как сказано выше, при импорте Python выполняет файл модуля целиком. Значит, всё, что стоит в модуле вне функций, сработает и при импорте. А иногда модуль хочется и запустить отдельно — например, чтобы прогнать его проверки. Как файлу понять, запустили его или импортировали?

В каждом файле есть встроенная переменная __name__ (два подчёркивания с каждой стороны). Python сам кладёт в неё значение: у файла, который запустили командой python файл.py, это строка "__main__", у импортированного модуля — его имя без .py. Проверим на двух файлах:

# utils.py
print("в utils.py __name__ =", __name__)

# main.py
import utils

print("в main.py __name__ =", __name__)

Запуск python main.py:

в utils.py __name__ = utils
в main.py __name__ = __main__

Запуск python utils.py:

в utils.py __name__ = __main__

Отсюда стандартная строка в конце файла:

if __name__ == "__main__":
    main()
  • в main.py она запускает программу только при прямом запуске. Когда в главе 3 вы собирали exe, PyInstaller запускал именно этот файл — условие выполнится;
  • в utils.py под этой строкой удобно держать проверки модуля: при python utils.py они выполнятся, а при импорте из main.py — нет, и не будут мусорить в выводе программы.

Проверьте себя

Задание. Программу запускают командой python main.py. Что окажется в первой строке вывода? Введите строку целиком.

Исходный код для этого задания:

# ===== файл raschet.py =====
def share(total, people):
    return total / people


print(__name__)

# ===== файл main.py =====
import raschet

print(raschet.share(900, 3))

Подсказка: Какое значение Python кладёт в __name__ импортированному модулю? Оно связано с именем файла.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Теперь тот же файл raschet.py запускают сам по себе: python raschet.py. Что он напечатает? Введите вывод.

Исходный код для этого задания:

# ===== файл raschet.py =====
def share(total, people):
    return total / people


print(__name__)

Подсказка: Кто здесь запускаемый файл, а кто импортированный модуль? Вспомните, какое значение получает __name__ у запускаемого файла.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Программу запускают командой python main.py. Сколько строк она напечатает? Введите число.

Исходный код для этого задания:

# ===== файл vremya.py =====
def minutes(text):
    h, m = text.split(":")
    return int(h) * 60 + int(m)


print("модуль время загружен")

if __name__ == "__main__":
    assert minutes("1:30") == 90
    assert minutes("0:05") == 5
    print("проверка 1 пройдена")
    print("проверка 2 пройдена")

# ===== файл main.py =====
from vremya import minutes

for t in ["0:45", "1:10", "2:00"]:
    print(minutes(t))

Подсказка: Разделите модуль на две части: что выполняется при любом импорте, а что спрятано под условием? Выполнится ли условие, если модуль импортировали?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Проверка функций на маленьких данных: assert

Окно и exe — последний шаг. Сначала убедитесь, что функции считают верно, на данных, где ответ известен заранее. Для этого есть инструкция assert: после неё пишется условие, которое обязано быть истинным.

assert parse_amount("1 200,50") == 1200.5   # верно — ничего не происходит
assert parse_amount("99") == 100.0         # неверно — программа останавливается

Если условие истинно, assert молчит. Если ложно, программа останавливается с ошибкой AssertionError и показывает строку проверки, которая не прошла. Если функция внутри проверки сама упала — например float не смог разобрать строку, — вы увидите её ошибку, обычно ValueError. В обоих случаях сломанное место видно сразу.

Как подбирать проверки:

  • 2–4 проверки на функцию обработки, данные — прямо в коде, 2–3 строки, ответ посчитан в уме;
  • обычный случай ("99"), случай из ТЗ ("1 200,50", "кафе ") и пустой ([] — пустая папка);
  • проверки — в utils.py под if __name__ == "__main__":, запуск — python utils.py.

Вот проверки «Разборщика чеков» в конце utils.py:

if __name__ == "__main__":
    assert parse_amount("1 200,50") == 1200.5
    assert parse_amount("99") == 99.0
    rows = [{"категория": "Кафе", "сумма": "100"},
            {"категория": "кафе ", "сумма": "50,5"},
            {"категория": "такси", "сумма": "300"}]
    assert totals_by_category(rows) == {"кафе": 150.5, "такси": 300.0}
    assert totals_by_category([]) == {}
    print("utils.py: все проверки пройдены")

Запуск python utils.py:

utils.py: все проверки пройдены

А так выглядит запуск всей программы python main.py на двух выгрузках из урока — проверки модуля при этом молчат:

# vygruzki/2026-09-a.csv
дата;категория;сумма
2026-09-02;Продукты;1 240,50
2026-09-03;кафе;380
2026-09-05;Транспорт;65

# vygruzki/2026-09-b.csv
дата;категория;сумма
2026-09-09;продукты;2 015
2026-09-10;Кафе;420,30
кафе: 800.30
продукты: 3255.50
транспорт: 65.00

assert — инструмент разработчика, а не проверка ввода человека. Плохую строку в выгрузке или пустое поле в окне по-прежнему ловят try/except и понятное сообщение, как в главе 2.

Положите utils.py, main.py и папку vygruzki с двумя CSV из урока в одну папку и запустите сначала python utils.py, потом python main.py. Затем испортите parse_amount — уберите .replace(" ", "") — и снова запустите python utils.py.

Проверьте себя

Контрольный вопрос. Файл проверок запустили. Какая проверка остановит программу первой?

Исходный код для этого задания:

def parse_amount(text):
    return float(text.replace(",", "."))


assert parse_amount("350") == 350.0
assert parse_amount("12,5") == 12.5
assert parse_amount("1 200") == 1200.0
assert parse_amount("0,99") == 0.99
print("всё верно")
Аassert parse_amount(«350») == 350.0
Бassert parse_amount(«12,5») == 12.5
Вassert parse_amount(«1 200») == 1200.0
Гassert parse_amount(«0,99») == 0.99

Подсказка: Пройдите проверки по порядку и для каждой подставьте строку в тело функции: что останется после replace и сможет ли float это разобрать?

Онлайн-проверка ответа появится позже

Задание. Соберите программу из двух файлов сами. main.py уже написан (он ниже, менять его не нужно). Напишите рядом модуль vremya.py с двумя функциями: minutes(text) переводит запись «часы:минуты» в число минут, fmt(total) превращает число минут в строку вида «2 ч 5 мин». В конце модуля, в блоке, который выполняется только при прямом запуске файла, поставьте по одной проверке assert на каждую функцию. Запустите python main.py. В поле ответа — число из последней строки вывода.

Исходный код для этого задания:

from vremya import minutes, fmt

log = ["0:45", "1:20", "0:35", "2:05", "0:50", "1:15", "0:40", "1:30", "0:25", "2:10"]
total = sum(minutes(t) for t in log)
print(fmt(total))
print(total)

Подсказка: Для minutes пригодится split(«:») и int, для fmt — целочисленное деление и остаток от деления на 60. Сначала добейтесь, чтобы python vremya.py прошёл проверки, потом запускайте main.py.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Онлайн-проверка ответа появится позже

Миссия: разложить «простыню»

Так выглядит программа, которую писали «сверху вниз» без функций. Она работает, но проверить её по частям, прикрутить к ней окно или командную строку трудно. Разложите её по правилам урока.

Задание. Проектная миссия «Из простыни — в два файла». Ниже программа одним куском: она складывает траты по категориям из выгрузки (данные вшиты в переменную DATA, 52 строки) и считает пропущенные строки. Разложите её на два файла в одной папке:
— utils.py: функция parse_line(line) возвращает пару (категория, сумма) или выбрасывает ValueError для плохой строки; функция add_to_totals(totals, category, value) добавляет сумму в словарь; новая функция top_category(totals) возвращает категорию с наибольшей суммой; в блоке только для прямого запуска — проверки assert для всех трёх функций;
— main.py: DATA, функция main() с циклом и выводом итогов, try/except вокруг parse_line, в конце строка «Больше всего: категория сумма», где сумма округлена round() до целого; вызов main() — только при прямом запуске файла.
Итоги по категориям и число пропущенных строк должны совпасть с выводом исходной программы. Приложите оба файла .py; в поле ответа — число из последней строки вывода вашего main.py.

Принято, если:
1. число совпадает с эталоном;
2. приложены utils.py и main.py, которые это число выдают на данных из условия;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. в utils.py нет ни print, ни чтения DATA — только функции и проверки в блоке прямого запуска;
4. python utils.py проходит проверки, а python main.py их не печатает;
5. каждую функцию можно назвать одним глаголом без «и».

Исходный код для этого задания:

DATA = """
2026-09-01;продукты;720,59
2026-09-01;Связь;1 610,83
2026-09-02;аптека;3477
2026-09-02;Транспорт;426
2026-09-03;Транспорт;2 871,17
2026-09-03;Продукты;1 194,37
2026-09-04;Кафе ;552
2026-09-04;Продукты;628,32
2026-09-05;Продукты;962,87
2026-09-06;Транспорт;3 302,48
2026-09-06;кафе;1707
2026-09-07;продукты;391
2026-09-07;Подарки;511
2026-09-08;продукты;1185
2026-09-08;Подарки;608,15
2026-09-09;кафе;822
2026-09-09;Транспорт;523
2026-09-10;кафе
2026-09-11;аптека;1527,97
2026-09-11;Подарки;2 778
2026-09-12;аптека;2923
2026-09-12;продукты;
2026-09-13;Подарки;917
2026-09-13;аптека;779
2026-09-14;аптека;1011
2026-09-14;Подарки;975
2026-09-15;кафе;0
2026-09-16;аптека;2 079
2026-09-16;Связь;-150
2026-09-17;Транспорт;671,75
2026-09-17;Транспорт;3250,15
2026-09-18;Кафе ;1 675,49
2026-09-18;Связь;-150
2026-09-19;кафе;0
2026-09-19;продукты;3306
2026-09-20;кафе;2473
2026-09-21;Подарки;1051
2026-09-21;продукты;3 422,78
2026-09-22;аптека
2026-09-22;Транспорт;106
2026-09-23;продукты;2 723
2026-09-23;аптека;0
2026-09-24;Продукты;2192,59
2026-09-24;Продукты;2 442
2026-09-25;кафе;2 992
2026-09-26;Связь;3 128,66
2026-09-26;Связь;1632,79
2026-09-27;Кафе ;3122,75
2026-09-27;кафе;1633
2026-09-28;Транспорт;406
2026-09-28;Продукты;1 322
2026-09-29;Продукты;808
"""

totals = {}
bad = 0
for line in DATA.strip().splitlines():
    parts = line.split(";")
    if len(parts) != 3:
        bad += 1
        continue
    date, category, amount = parts
    category = category.strip().lower()
    try:
        value = float(amount.replace(" ", "").replace(",", "."))
    except ValueError:
        bad += 1
        continue
    if value <= 0:
        bad += 1
        continue
    totals[category] = totals.get(category, 0) + value
for category in sorted(totals):
    print(f"{category}: {totals[category]:.2f}")
print("Пропущено строк:", bad)

Подсказка: Начните с parse_line: перенесите в неё всё, что исходная программа делает с одной строкой, а каждое continue после bad += 1 замените на raise ValueError. Счётчик пропусков останется в main() в ветке except.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Онлайн-проверка ответа появится позже

Что дальше

Теперь вы умеете выбрать задачу для своего проекта, описать её в ТЗ на одну страницу, разложить программу на функции ввода, обработки и вывода по одной работе на функцию, вынести их в свой модуль и подключить через import или from … import, разделить запуск и импорт строкой if __name__ == "__main__": и проверить функции через assert до всякого окна. В следующем уроке соберёте свою утилиту целиком, проверите её перед тем, как отдавать людям, и подготовитесь к защите.

← Назад · ↑ В начало урока · ⌂ К навигатору курса · Вперёд →

Школа Виктора Комлева