$ sudo teach IT
Модуль 10 · Финал · Урок 10.1

Финальный проект: TODO-менеджер

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

Практика~30 минутНовичокadd_task()mark_done()remove_done()список словарей

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

Как хранить список дел

Простыми словами. Представьте бумажный блокнот со списком дел. У каждой строки в нём два свойства: что нужно сделать и отмечена ли она галочкой. Сам блокнот — это список строк, а каждая строка — отдельная карточка с двумя полями. В Python эту же идею собирают из двух уже знакомых вам вещей: словаря для одной задачи и списка, который держит все эти словари подряд.

Официальный термин. Такую конструкцию называют списком словарей: list, где каждый элемент — отдельный dict с одинаковым набором ключей. Ключи словаря играют роль полей записи — как столбцы в таблице, только здесь каждая строка таблицы хранится в памяти как отдельный словарь. Такая пара «список + словари внутри» — самый частый способ представить набор однотипных записей без сторонних библиотек.

tasks = []

tasks.append({'title': 'Купить билеты', 'done': False})
tasks.append({'title': 'Позвонить в поддержку', 'done': True})

print(tasks[0]['title'])
print(tasks[1]['done'])
  • tasks = [] — заводим пустой список. Пока в нём нет ни одной задачи, но именно в нём будут жить все словари-задачи.
  • tasks.append({'title': 'Купить билеты', 'done': False}) — создаём словарь с двумя ключами: 'title' хранит текст задачи, 'done' хранит булево значение — отметку, выполнена ли она. Метод append добавляет этот словарь в конец списка tasks как единый элемент.
  • Вторая строка с append добавляет вторую задачу тем же способом, но со значением 'done': True — эта задача уже отмечена выполненной.
  • tasks[0]['title'] — сначала квадратные скобки [0] достают из списка tasks первый элемент, то есть первый словарь. Затем вторые квадратные скобки ['title'] достают из этого словаря значение по ключу 'title'. Напечатается Купить билеты.
  • tasks[1]['done'] — тем же способом берём второй словарь и его значение по ключу 'done'. Напечатается True.

Название и правописание ключей — это просто строки, которые вы выбираете сами. Если в одном месте программы написать 'title', а в другом по ошибке 'Title' — Python не подскажет опечатку, он решит, что это два разных ключа. Во всём проекте держите единый набор имён ключей: 'title' и 'done'.

Этап 1: добавление и вывод задач

Простыми словами. Вместо того чтобы в разных местах программы каждый раз вручную собирать словарь и звать append, полезно один раз описать, как добавляется задача, а дальше только вызывать этот рецепт по имени. То же самое с выводом: вместо ручного перебора списка каждый раз — один рецепт, который умеет красиво напечатать весь список.

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

def add_task(tasks, title):
    tasks.append({'title': title, 'done': False})


def list_tasks(tasks):
    for i, task in enumerate(tasks):
        status = 'выполнено' if task['done'] else 'не выполнено'
        print(f"{i}. {task['title']} — {status}")
  • def add_task(tasks, title): — функция принимает список задач и текст новой задачи.
  • tasks.append({'title': title, 'done': False}) — собирает словарь новой задачи и добавляет его в конец переданного списка. Новая задача всегда стартует невыполненной, поэтому 'done' сразу False.
  • def list_tasks(tasks): — функция принимает список задач и ничего не возвращает, только печатает.
  • for i, task in enumerate(tasks): — перебираем список. enumerate отдаёт на каждом шаге пару: i — порядковый номер элемента, начиная с 0, и сам элемент, который мы кладём в переменную task. Без enumerate пришлось бы отдельно заводить счётчик и увеличивать его вручную на каждом шаге.
  • status = 'выполнено' if task['done'] else 'не выполнено' — тернарное выражение: если task['done'] истинно, в status попадает строка 'выполнено', иначе — 'не выполнено'.
  • print(f"{i}. {task['title']} — {status}") — f-строка собирает одну читаемую строку: номер, точка, название задачи, тире, статус.

Заметьте: add_task не возвращает список через return и не создаёт его заново. Она меняет тот же список, который ей передали, прямо внутри вызова. После add_task(tasks, 'Полить цветы') список tasks снаружи функции уже содержит новую задачу — ничего дополнительно присваивать не нужно.

Этап 2: отметить выполненной и убрать лишнее

Простыми словами. Отметить задачу выполненной — это найти нужную карточку в блокноте по номеру и поставить на ней галочку, не трогая остальные. Убрать выполненные задачи — это переписать блокнот заново, оставив в нём только те строки, где галочки ещё нет.

Официальный термин. Первое — изменение значения по ключу у конкретного элемента списка через индексацию: tasks[index] достаёт словарь-задачу по номеру, а ['done'] = True меняет в нём значение поля. Второе — построение нового списка по условию, приём, который вы уже видели как генератор списка (list comprehension): [task for task in tasks if not task['done']] проходит по tasks и оставляет только те задачи, где условие not task['done'] истинно, то есть невыполненные.

def mark_done(tasks, index):
    tasks[index]['done'] = True


def remove_done(tasks):
    remaining = [task for task in tasks if not task['done']]
    tasks.clear()
    tasks.extend(remaining)
  • def mark_done(tasks, index): — функция принимает список задач и порядковый номер той, что нужно отметить.
  • tasks[index]['done'] = True — сначала tasks[index] достаёт из списка нужный словарь. Затем ['done'] = True меняет в этом словаре значение по ключу 'done' на True. Больше в словаре ничего не трогаем — 'title' остаётся прежним.
  • remaining = [task for task in tasks if not task['done']] — генератор списка. Он читается так же, как обычный цикл: «для каждой task из tasks, если not task['done'] истинно — положить task в новый список». Результат кладём в переменную remaining: это новый список, отдельный от tasks.
  • tasks.clear() — метод списка, который удаляет из tasks все элементы, оставляя список пустым, но это тот же самый объект списка, а не новый.
  • tasks.extend(remaining) — метод списка, который добавляет в tasks сразу все элементы из remaining, один за другим. После этих двух строк tasks содержит те же задачи, что и remaining, но список остался тем же объектом, каким был снаружи функции.

Если внутри remove_done написать просто tasks = remaining, программа не сломается с ошибкой, но и не сработает: это создаст в функции локальную переменную tasks, указывающую на новый список, а тот список, что был снаружи, останется прежним и никак не изменится. Присваивание имени внутри функции никогда не подменяет объект снаружи — поэтому и нужны clear() и extend(), которые меняют содержимое существующего списка, а не создают новый.

Этап 3: фильтры и статистика

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

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

def filter_tasks(tasks, done):
    return [task for task in tasks if task['done'] == done]


def get_stats(tasks):
    total = len(tasks)
    done = len(filter_tasks(tasks, True))
    return {'total': total, 'done': done, 'active': total - done}
  • def filter_tasks(tasks, done): — функция принимает список задач и значение done, по которому фильтруем: True для выполненных, False для невыполненных.
  • return [task for task in tasks if task['done'] == done] — строим новый список из тех задач, у которых значение по ключу 'done' совпадает с переданным аргументом done, и сразу возвращаем его. Исходный список tasks при этом не меняется.
  • def get_stats(tasks): — функция принимает список задач и вернёт словарь со сводкой.
  • total = len(tasks) — len считает общее число задач в списке.
  • done = len(filter_tasks(tasks, True)) — сначала filter_tasks(tasks, True) отбирает только выполненные задачи, затем len считает, сколько их. Переиспользуем уже готовую функцию вместо того, чтобы писать фильтрацию заново.
  • return {'total': total, 'done': done, 'active': total - done} — собираем и возвращаем словарь из трёх чисел: всего задач, сколько выполнено и сколько осталось активных (разница между общим числом и выполненными).

Частые ошибки

Эти ловушки встречаются почти у каждого, кто впервые собирает такую программу. Взгляните на них заранее.

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

template = {'title': '', 'done': False}
tasks = [template, template, template]
tasks[0]['title'] = 'Первая'
print(tasks[1]['title'])

Здесь tasks содержит три ссылки на один и тот же словарь template, поэтому строка напечатает Первая, хотя вы меняли только tasks[0]. Правильно — создавать новый словарь {'title': ..., 'done': False} для каждой задачи отдельно, как в add_task.

Индекс вне диапазона. mark_done(tasks, 5) для списка из трёх задач вызовет IndexError: индексы у списка из трёх элементов — это только 0, 1 и 2. Номер, который показывает list_tasks, начинается с 0, а не с 1, потому что его выдаёт enumerate — про это легко забыть и передать в mark_done номер на единицу больше нужного.

Переприсваивание вместо изменения списка. Как уже отмечено выше: tasks = [...] внутри функции создаёт локальное имя и не влияет на список снаружи. Если функция должна изменить переданный список «на месте», используйте методы самого списка — append, clear, extend — или присваивание через срез tasks[:] = [...], а не замену имени переменной.

Что важно запомнить

  • Набор однотипных записей удобно хранить как список словарей: список отвечает за порядок и количество, а словарь — за поля одной записи.
  • Списки передаются в функцию по ссылке: tasks.append(...) или tasks[i]['done'] = True внутри функции меняют тот же список, что и снаружи.
  • Присваивание имени (tasks = ...) внутри функции не меняет список снаружи — для изменения «на месте» нужны методы списка (append, clear, extend) или срез tasks[:] = ....
  • enumerate(tasks) даёт одновременно номер элемента и сам элемент — удобно для нумерованного вывода.
  • Генератор списка [x for x in items if условие] одинаково хорошо подходит и для отбора части данных (фильтр), и для построения нового списка на замену старому.
  • Сводные показатели (сколько всего, сколько выполнено) удобно собирать в отдельный словарь — так их легко передавать и печатать одним куском.

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

10 вопросов

Этап 1: список задач

Начните собирать TODO-менеджер. Задача хранится как словарь с ключами title (строка) и done (булево значение). Список задач передаётся в функции параметром tasks.

Реализуйте:

  • add_task(tasks, title) — добавляет в конец списка tasks новую задачу {'title': title, 'done': False}.
  • list_tasks(tasks) — печатает каждую задачу отдельной строкой в формате "{номер}. {title} — {статус}", где номер начинается с 0, а статус — выполнено или не выполнено в зависимости от task['done']. Пример строки: 0. Купить хлеб — не выполнено.

Этап 2: выполнить и убрать

Premium

Этап 3: фильтры и статистика

Premium