Projeto Final: Gerenciamento de Notas Escolares (Parte 2)

Na Lição 34, completamos a camada de dados — operações CRUD para alunos e notas. Nesta lição, construímos a camada de lógica sobre a camada de dados: cálculos estatísticos, ranking, análise de distribuição de notas e outras regras de negócio centrais. A Lição 36 adicionará a camada de apresentação (interface de menu) para completar o sistema.


Implementação da Camada de Lógica

Esta camada depende das funções da camada de dados da Lição 34 (load_data, get_all_students, etc.).

Exemplo: Camada de Lógica de Estatísticas de Notas

PYTHON
# ====== Camada de Lógica: Estatísticas e Análise de Notas ======

import statistics
from grade_project_part1 import load_data, get_all_students


def calculate_student_average(student_id):
    """Calcula a média de notas de um aluno"""
    data = load_data()
    student = data["students"].get(student_id)
    if not student:
        return None

    scores = student["scores"].values()
    if not scores:
        return None

    return sum(scores) / len(scores)


def get_student_report(student_id):
    """Gera um boletim individual"""
    data = load_data()
    student = data["students"].get(student_id)
    if not student:
        return None

    report = {
        "name": student["name"],
        "class_name": student["class_name"],
        "scores": dict(student["scores"]),
    }

    scores = list(student["scores"].values())
    if scores:
        report["average"] = round(sum(scores) / len(scores), 1)
        report["max_score"] = max(scores)
        report["min_score"] = min(scores)
        report["total"] = sum(scores)
    else:
        report["average"] = None
        report["max_score"] = None
        report["min_score"] = None
        report["total"] = 0

    return report


def get_class_ranking(class_name=None):
    """Obtém o ranking da turma (por total de pontos decrescente)"""
    data = load_data()
    students = data["students"]

    # Coleta todos os alunos (ou uma turma específica)
    results = []
    for sid, info in students.items():
        if class_name and info["class_name"] != class_name:
            continue

        scores = list(info["scores"].values())
        total = sum(scores) if scores else 0
        avg = round(total / len(scores), 1) if scores else 0

        results.append({
            "student_id": sid,
            "name": info["name"],
            "class_name": info["class_name"],
            "total": total,
            "average": avg,
            "score_count": len(scores)
        })

    # Ordenar por total decrescente
    results.sort(key=lambda x: x["total"], reverse=True)

    # Adicionar ranking
    for i, r in enumerate(results, 1):
        r["rank"] = i

    return results


def get_subject_averages():
    """Calcula as médias de cada disciplina"""
    data = load_data()
    subjects = data["subjects"]
    students = data["students"]

    result = {}
    for subject in subjects:
        scores = []
        for info in students.values():
            if subject in info["scores"]:
                scores.append(info["scores"][subject])

        if scores:
            result[subject] = {
                "average": round(sum(scores) / len(scores), 1),
                "max": max(scores),
                "min": min(scores),
                "count": len(scores)
            }
        else:
            result[subject] = None

    return result


def get_score_distribution(subject=None):
    """Calcula a distribuição de notas em faixas"""
    data = load_data()
    students = data["students"]

    ranges = [
        ("90-100", 90, 101),
        ("80-89", 80, 90),
        ("70-79", 70, 80),
        ("60-69", 60, 70),
        ("0-59", 0, 60),
    ]

    if subject:
        # Contar distribuição para uma disciplina específica
        result = {label: 0 for label, _, _ in ranges}
        for info in students.values():
            if subject in info["scores"]:
                score = info["scores"][subject]
                for label, low, high in ranges:
                    if low <= score < high:
                        result[label] += 1
                        break
        return result
    else:
        # Contar distribuição entre todas as disciplinas combinadas
        result = {label: 0 for label, _, _ in ranges}
        for info in students.values():
            for score in info["scores"].values():
                for label, low, high in ranges:
                    if low <= score < high:
                        result[label] += 1
                        break
        return result


def get_students_by_class():
    """Agrupa alunos por turma"""
    data = load_data()
    students = data["students"]

    classes = {}
    for sid, info in students.items():
        cls = info["class_name"]
        if cls not in classes:
            classes[cls] = []
        classes[cls].append({
            "student_id": sid,
            "name": info["name"],
            "score_count": len(info["scores"])
        })

    return classes
▶ Experimente

Teste da Camada de Lógica

Exemplo: Teste Funcional da Camada de Lógica

PYTHON
if __name__ == "__main__":
    print("=== Boletim Individual ===")
    report = get_student_report("2024001")
    if report:
        print(f"Nome: {report['name']}")
        print(f"Turma: {report['class_name']}")
        for subject, score in report["scores"].items():
            print(f"  {subject}: {score}")
        print(f"Total: {report['total']}")
        print(f"Média: {report['average']}")
        print(f"Máx: {report['max_score']}")
        print(f"Mín: {report['min_score']}")

    print("\n=== Ranking da Turma ===")
    ranking = get_class_ranking()
    for r in ranking[:5]:
        print(f"#{r['rank']}: {r['name']} ({r['class_name']}) Total {r['total']}")

    print("\n=== Médias por Disciplina ===")
    averages = get_subject_averages()
    for subject, info in averages.items():
        if info:
            print(f"{subject}: Média {info['average']}, Máx {info['max']}, Mín {info['min']}")

    print("\n=== Distribuição de Notas ===")
    dist = get_score_distribution()
    for label, count in dist.items():
        bar = "#" * count
        print(f"{label}: {bar} {count} alunos")
▶ Experimente

Nota: O import acima assume que o arquivo da camada de dados se chama grade_project_part1.py. Modifique a declaração de import se o nome do arquivo for diferente. No sistema completo final, todo o código será mesclado em um único arquivo.


Design de Tratamento de Exceções

A camada de lógica lida com os seguintes casos excepcionais:

Cenário Valor de Retorno Descrição
Aluno não existe None ou resultado vazio Chamador verifica o valor de retorno
Sem dados de nota Valor estatístico é None Chamador exibe "Nenhuma nota ainda"
Dados vazios Lista vazia/dict vazio Não quebra, exibe mensagem amigável
Dados inválidos Exceção capturada Protegido por try-except

❓ Perguntas Frequentes

P: Como entender key=lambda na ordenação? R: O parâmetro key especifica o critério de ordenação. lambda item: item[1] significa "pegue o segundo elemento de cada item como chave de ordenação." Isso equivale a def get_score(item): return item[1] — lambda é apenas uma abreviação. P: Retornar None em vez de 0 não dificulta as coisas para o chamador? R: Retornar None distingue entre "nenhuma nota existe" e "a nota é realmente 0" — uma diferença significativa. O chamador pode usar if result is not None: para verificar, o que é mais seguro do que tratar erroneamente "sem dados" como "0 pontos."


📖 Resumo


📝 Atividades

  1. Iniciante (Dificuldade ⭐): Chame get_student_report() para visualizar um boletim completo de um aluno e verifique se a saída está correta.

  2. Intermediário (Dificuldade ⭐⭐): Adicione uma função get_top_n(n) à camada de lógica que retorna os N melhores alunos por total de pontos em toda a escola.

  3. Avançado (Dificuldade ⭐⭐⭐): Adicione uma função get_subject_pass_rate(subject, pass_score=60) à camada de lógica que calcula a taxa de aprovação para uma determinada disciplina (alunos com nota >= pass_score / total de alunos). Depois calcule e compare as taxas de aprovação entre todas as disciplinas.

100%