Python: Projeto: Notas Escolares (Pt.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.


1. 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

TEXT 📖 Somente leitura
# ====== 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


2. Teste da Camada de Lógica

▶ Exemplo: Teste Funcional da Camada de Lógica

TEXT 📖 Somente leitura
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")

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.


▶ Exemplo: Cálculo de Média por Turma (Dificuldade ⭐)

PYTHON
scores = {
    "2024001": {"name": "Alice", "class_name": "Turma 1", "scores": {"Português": 85, "Matemática": 92, "Inglês": 88}},
    "2024002": {"name": "Bob", "class_name": "Turma 1", "scores": {"Português": 78, "Matemática": 85, "Inglês": 90}},
    "2024003": {"name": "Charlie", "class_name": "Turma 2", "scores": {"Português": 92, "Matemática": 88, "Inglês": 95}},
    "2024004": {"name": "Diana", "class_name": "Turma 2", "scores": {"Português": 70, "Matemática": 75, "Inglês": 80}},
}

def class_averages(students):
    classes = {}
    for sid, info in students.items():
        cls = info["class_name"]
        if cls not in classes:
            classes[cls] = {}
        for subject, score in info["scores"].items():
            if subject not in classes[cls]:
                classes[cls][subject] = []
            classes[cls][subject].append(score)

    result = {}
    for cls, subjects in classes.items():
        result[cls] = {}
        for subject, vals in subjects.items():
            result[cls][subject] = round(sum(vals) / len(vals), 1)

    return result

averages = class_averages(scores)
for cls, subjects in averages.items():
    print(f"\n{cls}:")
    for subject, avg in subjects.items():
        print(f"  Média de {subject}: {avg}")
▶ Experimente

Saída:

TEXT 📖 Somente leitura
Turma 1:
  Média de Português: 81.5
  Média de Matemática: 88.5
  Média de Inglês: 89.0

Turma 2:
  Média de Português: 81.0
  Média de Matemática: 81.5
  Média de Inglês: 87.5


3. 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."


P: Por que separar a camada de lógica da camada de dados em vez de colocar tudo em um arquivo? R: Separação de responsabilidades torna o código mais fácil de manter e testar. A camada de dados lida com armazenamento (carregar/salvar), enquanto a camada de lógica lida com computação (estatísticas, ranking). Quando os requisitos mudam — por exemplo, mudar de JSON para SQLite — você só modifica a camada de dados. A camada de lógica permanece intacta. Isso também torna os testes unitários de cada camada independentemente muito mais simples.

📖 Resumo



📝 Exercícios

  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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%