Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 20 из 20 Начальный 25 мин 150 XP

Проект: анализ оценок класса на циклах Python

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

Редакция Питоники

Финальный проект раздела собирает в одной программе всё, чему он научил: перебор, аккумуляторы, условия, any и all, умножение строки на число. Задача — отчёт об оценках класса: из журнала получить средний балл, лучшего ученика, список отстающих и гистограмму. Отчёт печатается тремя кусками, и каждый кусок — самостоятельная программа со своими данными: её можно запустить отдельно и проверить свой кусок отчёта. Маленькие проверяемые шаги вместо одного огромного скрипта — так собирается любой отчёт по данным.

Кусок отчётаЧто считаетКакой приём
Кусок 1Средний балл, лучший результат, отличникиАккумулятор, максимум, фильтр-список
Кусок 2Кто ниже порога, есть ли проблемныеФильтр с печатью, any и all
Кусок 3Распределение оценок и гистограммаСчётчик-словарь, умножение строки

Данные: журнал класса

Данные проекта — словарь: имя ученика — его оценка. Восемь учеников, оценки от двойки до пятёрки. Словарь выбран не случайно: обход по items() отдаёт пары «имя — оценка» в порядке вставки, а по values() — одни оценки, когда имена не нужны. Прежде чем считать, посмотрим на данные глазами:

шаг 0: показать журнал
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

for name, mark in grades.items():
    print(f'{name:<7}{mark}')
Вывод
Анна   5
Борис  4
Вера   3
Глеб   5
Даша   4
Егор   2
Женя   4
Зина   3

Первый шаг любого анализа — показать данные как есть: пары столбиком, ровно, через f-строку с шириной. Так проект начинается всегда: сначала глаза на данные, потом вычисления. Теперь три куска отчёта.

Кусок 1: состав, средний балл, лучший

кусок 1: три ответа сразу
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

total = 0
for mark in grades.values():
    total += mark
print('Учеников:', len(grades))
print('Средний балл:', total / len(grades))

best_name = ''
best_mark = 0
for name, mark in grades.items():
    if mark > best_mark:
        best_mark = mark
        best_name = name
print('Лучший результат:', best_name, '-', best_mark)

excellent = []
for name, mark in grades.items():
    if mark == 5:
        excellent.append(name)
print('Отличники:', excellent)
Вывод
Учеников: 8
Средний балл: 3.75
Лучший результат: Анна - 5
Отличники: ['Анна', 'Глеб']

В одном куске живут три знакомых приёма. Аккумулятор total собирает сумму по values(), деление на len даёт среднее: тридцать на восемь — ровно три и три четверти, Python печатает 3.75 без хвостов. Каркас максимума — пара best_name, best_mark со строгим сравнением: Глеб тоже отличник, но Анна стоит в журнале раньше, и побеждает первый встреченный. Фильтр-список собирает всех пятёрочников сразу: максимум отвечает «кто лучший», фильтр — «кто соответствует условию».

фрагмент: среднее одной строкой
average = sum(grades.values()) / len(grades)
Фрагмент: переменная grades объявлена выше. Встроенная sum заменяет цикл-аккумулятор; в проекте считаем циклом, чтобы попрактиковаться.

Кусок 2: кто ниже порога

кусок 2: проблемные и вердикты
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

print('Ниже порога:')
for name, mark in grades.items():
    if mark < 4:
        print(' ', name, '-', mark)

risky = any(m < 4 for m in grades.values())
print('Есть проблемные:', risky)
print('Все выше двойки:', all(m >= 3 for m in grades.values()))
Вывод
Ниже порога:
  Вера - 3
  Егор - 2
  Зина - 3
Есть проблемные: True
Все выше двойки: False

Проверка mark < 4 отбирает тройки и двойки вместе с именами — учительский список «кого тянуть». После него — строка-вердикт через any из урока про any и all: правда ли, что проблемные есть. Здесь any кажется избыточным — список уже напечатан, но приём масштабируется: на журнале из тысячи записей any ответит, не пробегая его целиком, короткое замыкание остановит проверку на первом же нарушителе. А all даёт обратный взгляд: все ли выше двойки — нет, Егор сломал всеобщность.

самая слабая оценка
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

worst = 5
for mark in grades.values():
    if mark < worst:
        worst = mark
print('Худшая оценка:', worst)
Вывод
Худшая оценка: 2

Каркас максимума работает и в обратную сторону: старт с верхней границы и знак меньше — и отчёт знает самую слабую оценку класса. Тот же перебор, зеркальное сравнение, никаких новых приёмов.

Кусок 3: распределение и гистограмма

кусок 3: счёт, мода, гистограмма
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

counts = {}
for mark in grades.values():
    counts[mark] = counts.get(mark, 0) + 1
print(counts)

best_mark = 0
best_count = 0
for mark, count in counts.items():
    if count > best_count:
        best_count = count
        best_mark = mark
print('Мода журнала: оценка', best_mark)
print()
for mark in [5, 4, 3, 2]:
    print(mark, ':', '#' * counts.get(mark, 0))
Вывод
{5: 2, 4: 3, 3: 2, 2: 1}
Мода журнала: оценка 4

5 : ##
4 : ###
3 : ##
2 : #

Кусок в три хода. Первый — счётчик-словарь из курса словарей: get с умолчанием ноль достаёт текущий счёт, к нему прибавляется единица; ключи в печатном словаре стоят в порядке первого появления. Второй — знакомый поиск максимума, но уже по счётам: мода журнала — четвёрка, она встретилась трижды. Третий — гистограмма: умножение строки на число превращает счёт в столько решёток, сколько учеников получили оценку. Обход по готовому списку [5, 4, 3, 2] фиксирует порядок строк от пятёрки к двойке, как в школьном журнале, а get с нулём страхует оценку, которой в классе нет: столбик будет пустым, но строка останется и отчёт не упадёт.

фрагмент: гистограмма ровными колонками
print(f'{mark}: {counts.get(mark, 0):>2}  ' + '#' * counts.get(mark, 0))
Фрагмент: переменные mark и counts объявлены выше. Спецификация >2 прижимает счёт вправо в колонке шириной два — числа в отчёте стоят ровно.

Бонус: вердикт каждому

словесная оценка по журналу
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

for name, mark in grades.items():
    if mark == 5:
        verdict = 'отлично'
    elif mark == 4:
        verdict = 'хорошо'
    elif mark == 3:
        verdict = 'удовлетворительно'
    else:
        verdict = 'надо подтянуть'
    print(name, '-', verdict)
Вывод
Анна - отлично
Борис - хорошо
Вера - удовлетворительно
Глеб - отлично
Даша - хорошо
Егор - надо подтянуть
Женя - хорошо
Зина - удовлетворительно

Цепочка if/elif переводит число в слово для каждого ученика — тот же перебор по items(), только вместо подсчёта внутри цикла живёт выбор ветки. Обратите внимание на отступы: print стоит на уровне тела внешнего цикла, поэтому выполняется для каждой пары, а не один раз после цикла.

Проценты: доля от общего

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

сколько процентов на каждую оценку
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

marks = list(grades.values())
for mark in [5, 4, 3, 2]:
    count = sum(1 for m in marks if m == mark)
    share = round(count * 100 / len(marks))
    print(mark, '-', count, 'учеников,', share, '%')
Вывод
5 - 2 учеников, 25 %
4 - 3 учеников, 38 %
3 - 2 учеников, 25 %
2 - 1 учеников, 12 %

38 процентов — это 3 из 8: 37,5 округлилось до 38. Функция round возвращает целое число, и в отчёте дробная часть процента обычно не нужна. Но если проценты складывают в сумму, округление даёт расхождение: 25 + 38 + 25 + 12 даёт ровно 100, а вот на других данных может выйти 101. Тогда проценты считают от общей суммы и проверяют сумму.

Вертикальная гистограмма

Горизонтальная гистограмма из третьего куска показывает столбики в строку. Вертикальная — привычнее: оценки идут сверху вниз, а ученики стоят в колонках. Делается она двумя вложенными циклами.

гистограмма столбиками
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

marks = list(grades.values())
for level in range(5, 1, -1):
    row = ''
    for m in marks:
        row += '#' if m == level else ' '
    print(f'{level} |{row.rstrip()}')
Вывод
5 |#  #
4 | #  # #
3 |  #    #
2 |     #

Внешний цикл идёт по оценкам сверху вниз, внутренний — по ученикам слева направо. Каждая пара «оценка, ученик» решается: если у ученика такая оценка — рисуем решётку, иначе пробел. Метод rstrip убирает хвостовые пробелы, иначе строки выравнивались бы по разной длине. Это ровно тот случай, где вложенные циклы из восьмого урока работают как надо.

Группа риска: тройки и двойки вместе

В школьной практике отстающих удобно собирать одним списком, а не двумя. Фильтр с условием «меньше или равно» делает это одной строкой.

кто в группе риска
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

at_risk = []
for name, mark in grades.items():
    if mark <= 3:
        at_risk.append(name)

print('группа риска:', at_risk)
print('всего:', len(at_risk), 'из', len(grades))
Вывод
группа риска: ['Вера', 'Егор', 'Зина']
всего: 3 из 8

Трое из восьми — 37,5 процента класса. Список собирается в порядке обхода словаря, то есть в порядке вставки: Вера, Егор, Зина. Условие <= вместо < принципиально: при < тройки в список не попали бы и группа риска вышла бы из одного человека.

фрагмент: тот же список одной строкой
at_risk = [name for name, mark in grades.items() if mark <= 3]
Фрагмент: словарь grades объявлен выше. Генератор списка даёт тот же результат — порядок обхода словаря сохраняется, список собирается в том же порядке.

Что стоит проверить перед сдачей отчёта

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

  • сложились ли части отчёта: средний балл, количество учеников и сумма оценок должны сходиться;
  • не пуст ли журнал — на пустом словаре деление на len даст ZeroDivisionError;
  • все ли оценки попали в счётчик: сумма счётчиков по всем оценкам равна числу учеников;
  • одинаков ли порядок в горизонтальной и вертикальной гистограммах;
  • не потеряны ли ученики с оценкой вне ожидаемого диапазона — например, с шестёркой или единицей.

Последний пункт стоит проверить первым. Если в словаре появится шестёрка, счётчик её учтёт, а гистограмма по списку [5, 4, 3, 2] — нет: столбика для шестёрки не будет, и ученик исчезнет из картинки. Цикл по ключам счётчика напечатал бы всё, но потерял бы порядок от пятёрки к двойке. Тот случай, когда приходится выбирать: полнота или порядок.

отчёт, который не теряет оценки
grades = {
    'Анна': 5, 'Борис': 4, 'Вера': 3,
    'Глеб': 5, 'Даша': 4, 'Егор': 2,
    'Женя': 4, 'Зина': 3,
}

counts = {}
for mark in grades.values():
    counts[mark] = counts.get(mark, 0) + 1

print('всего оценок:', sum(counts.values()))
print('всего учеников:', len(grades))
for mark in sorted(counts, reverse=True):
    print(mark, '->', counts[mark])
Вывод
всего оценок: 8
всего учеников: 8
5 -> 2
4 -> 3
3 -> 2
2 -> 1

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

Раздел «Циклы» окончен: от первого for по списку до программы, которая читает журнал и отвечает на четыре вопроса о данных. Цикл — самый рабочий инструмент программиста: списки обрабатываются перебором, строки разбираются посимвольно, словари обходит items(). Логичный следующий шаг — курс «Словари»: журналы, счётчики и частотные таблицы станут основной работой, а не бонусом к циклам.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

marks = [4, 2, 5]
total = 0
for m in marks:
    total += m
print(total // len(marks))
d = {}
for m in [3, 4, 3]:
    d[m] = d.get(m, 0) + 1
print(d)
marks = [4, 4, 5]
best = 0
for m in marks:
    if m > best:
        best = m
print(best)
Проверь себя
0 / 8

1. Каким каркасом в проекте считается средний балл?

2. Почему в поиске лучшего ученика сравнение строгое — mark > best_mark?

3. Что вернёт any(m < 4 for m in grades.values()), если в классе есть двойка?

4. Как в проекте печатается гистограмма?

5. Что вернёт counts.get(1, 0), если единиц в журнале не было?

6. Как в проекте собрать список учеников с оценкой не выше тройки?

7. Зачем в вертикальной гистограмме используют rstrip()?

8. Что проверит сумма счётчиков по всем оценкам?

Карточки терминов
Запомнено: 0 / 9
Практика

Дан журнал grades = {'Аня': 5, 'Борис': 3, 'Вера': 4, 'Глеб': 2}. Выведи количество учеников, средний балл и список имён тех, у кого оценка выше тройки.

practice.py
Вопросы и ответы по уроку

Как посчитать средний балл списка оценок на Python?

Аккумулятором: total = 0, в цикле total += mark, после цикла total / len(marks). Для встроенного решения — sum(marks) / len(marks). На пустом списке будет ZeroDivisionError, поэтому данные проверяют перед делением.

Урок 11: счётчики и аккумуляторы

Как найти ученика с максимальной оценкой?

Каркасом максимума: пара переменных имя-оценка и строгое сравнение в цикле по items(). При равных оценках первым останется тот, кто встретился раньше — порядок словаря стабилен, результат воспроизводим.

Как построить гистограмму символами в консоли?

Умножением строки на число: '#' * count печатает столько решёток, сколько насчитал счётчик. Счёт собирается словарём counts.get(mark, 0) + 1 — или Counter из курса словарей, если ручной счёт не нужен.

Урок 11 курса словарей: Counter

Что учить после курса по циклам?

Курс «Словари»: журналы, счётчики и частотные таблицы — следующая ступень работы с данными, и весь этот курс опирается на циклы. Как альтернатива — функции: куски отчёта проекта можно свернуть в переиспользуемые блоки.

Курс Словари: урок 1

Как посчитать процент учеников с какой-либо оценкой?

Разделить количество на общее число учеников: count * 100 / len(grades). Важно, чтобы делитель был один на весь отчёт: процент от числа учеников и процент от суммы баллов — разные базы, и смешивать их нельзя.

Как нарисовать вертикальную гистограмму оценок?

Два вложенных цикла: внешний идёт по оценкам от пяти к двойке, внутренний — по ученикам. Внутри сравниваем оценку ученика с текущей и ставим решётку или пробел. rstrip убирает хвостовые пробелы, чтобы строки были одинаковой длины.

Урок 8: вложенные циклы

Как понять, что в отчёте никто не потерялся?

Сложить счётчики по всем оценкам: сумма должна равняться числу учеников. Если меньше — какая-то запись не попала в счётчик или попала дважды. Простая сверка, которая ловит ошибку до того, как её увидит учитель.

Понравился урок? Сошлитесь на него

«Маленькие проверяемые шаги вместо одного огромного скрипта — так собирается любой отчёт по данным.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.