Python: مشروع: إدارة الدرجات (ج.2)

في الدرس 34، أكملنا طبقة البيانات — عمليات CRUD للطلاب والدرجات. في هذا الدرس، نبني طبقة المنطق فوق طبقة البيانات: الحسابات الإحصائية، الترتيب، تحليل توزيع الدرجات، ومنطق الأعمال الأساسي الآخر. الدرس 36 سيضيف طبقة العرض (واجهة القائمة) لإكمال النظام.


1. تنفيذ طبقة المنطق

هذه الطبقة تعتمد على دوال طبقة البيانات من الدرس 34 (load_data, get_all_students، إلخ.).

▶ مثال: طبقة منطق إحصائيات الدرجات

TEXTN 📖 للعرض فقط
# ====== طبقة المنطق: إحصائيات وتحليل الدرجات ======

import statistics
from grade_project_part1 import load_data, get_all_students


def calculate_student_average(student_id):
    """احسب متوسط درجة طالب واحد"""
    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):
    """أنشئ بطاقة تقرير فردية"""
    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):
    """احصل على ترتيب الفصل (حسب المجموع الإجمالي تنازلياً)"""
    data = load_data()
    students = data["students"]

    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)
        })

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

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

    return results


def get_subject_averages():
    """احسب متوسطات درجات كل مادة"""
    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):
    """احسب توزيع الدرجات عبر النطاقات"""
    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:
        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:
        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():
    """جمّع الطلاب حسب الفصل"""
    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
124 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

الناتج:

TEXT 📖 للعرض فقط
// Executed successfully


2. اختبار طبقة المنطق

▶ مثال: اختبار وظيفي لطبقة المنطق

TEXTN
if __name__ == "__main__":
    print("=== Individual Report Card ===")
    report = get_student_report("2024001")
    if report:
        print(f"Name: {report['name']}")
        print(f"Class: {report['class_name']}")
        for subject, score in report["scores"].items():
            print(f"  {subject}: {score}")
        print(f"Total: {report['total']}")
        print(f"Average: {report['average']}")
        print(f"Max: {report['max_score']}")
        print(f"Min: {report['min_score']}")

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

    print("\n=== Subject Averages ===")
    averages = get_subject_averages()
    for subject, info in averages.items():
        if info:
            print(f"{subject}: Avg {info['average']}, Max {info['max']}, Min {info['min']}")

    print("\n=== Score Distribution ===")
    dist = get_score_distribution()
    for label, count in dist.items():
        bar = "#" * count
        print(f"{label}: {bar} {count} students")
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
// Executed successfully

▶ مثال: حساب متوسط الصف (الصعوبة ⭐)

PYTHON
scores = {
    "2024001": {"name": "Alice", "class_name": "Class 1", "scores": {"Chinese": 85, "Math": 92, "English": 88}},
    "2024002": {"name": "Bob", "class_name": "Class 1", "scores": {"Chinese": 78, "Math": 85, "English": 90}},
    "2024003": {"name": "Charlie", "class_name": "Class 2", "scores": {"Chinese": 92, "Math": 88, "English": 95}},
    "2024004": {"name": "Diana", "class_name": "Class 2", "scores": {"Chinese": 70, "Math": 75, "English": 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"  {subject} average: {avg}")
▶ جرّب الكود

المخرجات:

TEXT 📖 للعرض فقط
Class 1:
  Chinese average: 81.5
  Math average: 88.5
  English average: 89.0

Class 2:
  Chinese average: 81.0
  Math average: 81.5
  English average: 87.5


3. تصميم معالجة الاستثناءات

طبقة المنطق تتعامل مع الحالات الاستثنائية التالية:

السيناريو القيمة المُرجعة الوصف
الطالب غير موجود None أو نتيجة فارغة المتصل يفحص القيمة المُرجعة
لا توجد بيانات درجة قيمة الإحصاء None المتصل يعرض "لا توجد درجات بعد"
بيانات فارغة قائمة فارغة/قاموس فارغ لن ينهار، يعرض رسالة ودية
بيانات غير صالحة استثناء مُلتقط محمي بواسطة try-except


❓ أسئلة شائعة

س كيف أفهم key=lambda في الترتيب؟
ج معامل key يُحدد معيار الترتيب. lambda item: item[1] تعني "خذ العنصر الثاني من كل عنصر كمفتاح ترتيب." هذا يعادل def get_score(item): return item[1] — lambda مجرد اختصار.
س أليس إرجاع None بدلاً من 0 يجعل الأمور أصعب للمتصل؟
ج إرجاع None يميِّز بين "لا توجد درجة" و "الدرجة هي 0 فعلياً" — فرق ذو معنى. يمكن للمتصل استخدام if result is not None: للتحقق، وهو أكثر أماناً من معاملة "لا توجد بيانات" خطأً على أنها "0 نقطة."

س لماذا نفصل طبقة المنطق عن طبقة البيانات بدلاً من وضع كل شيء في ملف واحد؟
ج فصل المسؤوليات يجعل الكود أسهل في الصيانة والاختبار. طبقة البيانات تتعامل مع التخزين (تحميل/حفظ)، بينما طبقة المنطق تتعامل مع الحساب (الإحصائيات، الترتيب). عندما تتغير المتطلبات — على سبيل المثال، التبديل من JSON إلى SQLite — تعدّل طبقة البيانات فقط. طبقة المنطق تبقى كما هي. هذا يجعل اختبار كل طبقة على حدة أبسط بكثير.

📖 ملخص


📝 تمارين

  1. مبتدئ (الصعوبة ⭐): استدعِ get_student_report() لعرض بطاقة تقرير كاملة لطالب واحد وتحقق من صحة المخرجات.

  2. متوسط (الصعوبة ⭐⭐): أضف دالة get_top_n(n) إلى طبقة المنطق تُرجع أفضل N طالباً حسب المجموع الإجمالي في المدرسة بأكملها.

  3. متقدم (الصعوبة ⭐⭐⭐): أضف دالة get_subject_pass_rate(subject, pass_score=60) إلى طبقة المنطق تحسب نسبة النجاح لمادة معينة (عدد الطلاب بدرجة >= pass_score / إجمالي الطلاب). ثم احسب وقارن نسب النجاح عبر جميع المواد.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%