За весь курс вы разобрали переменные, условия, циклы, строки, списки, словари и функции по отдельности, каждую тему — на своём маленьком примере. Настоящая программа так не выглядит: в ней разные темы работают вместе. 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. Купить хлеб — не выполнено.