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

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


1. نظرة عامة على المشروع

TEXT 📖 للعرض فقط
نظام إدارة درجات الطلاب
├── الدرس 34: طبقة البيانات — تحليل المتطلبات + استمرار JSON/CSV
├── الدرس 35: طبقة المنطق — إحصائيات الدرجات + الترتيب + معالجة الاستثناءات
└── الدرس 36: طبقة العرض — واجهة القائمة + البحث + التقارير


2. مواصفات المتطلبات

TEXT 📖 للعرض فقط
1. إدارة الطلاب
   ├── إضافة طالب (الاسم، رقم الطالب، الفصل)
   └── حذف طالب (برقم الطالب)

2. إدارة الدرجات
   ├── إدخال الدرجات (برقم الطالب لمواد متعددة)
   ├── تعديل الدرجات
   └── حذف سجلات الدرجات

3. الإحصائيات والتحليل
   ├── بطاقة تقرير فردية
   ├── ترتيب الفصل
   ├── متوسطات المواد
   └── توزيع الدرجات

4. استمرارية البيانات
   ├── تخزين البيانات بتنسيق JSON في ملف
   └── تحميل تلقائي عند بدء التشغيل


3. تصميم هيكل البيانات

▶ مثال: هيكل بيانات JSON

PYTHON
# البيانات مخزنة بتنسيق JSON
{
    "students": {
        "2024001": {
            "name": "Zhang San",
            "class_name": "Class 1",
            "scores": {
                "Chinese": 85,
                "Math": 92,
                "English": 78
            }
        },
        "2024002": {
            "name": "Li Si",
            "class_name": "Class 1",
            "scores": {
                "Chinese": 90,
                "Math": 88,
                "English": 95
            }
        }
    },
    "subjects": ["Chinese", "Math", "English"],
    "classes": ["Class 1", "Class 2"]
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
# Executed successfully
💡 لماذا استخدام رقم الطالب كمفتاح؟ استخدام رقم الطالب (سلسلة نصية) كمفتاح القاموس يجعل عمليات البحث O(1) — سواء كان هناك 10 أو 10,000 طالب، سرعة البحث هي نفسها.



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

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

PYTHON 📖 للعرض فقط
import json
import os

DATA_FILE = "grade_system.json"

# ====== هيكل البيانات الافتراضي ======
DEFAULT_DATA = {
    "students": {},
    "subjects": ["Chinese", "Math", "English"],
    "classes": []
}


# ====== طبقة البيانات ======

def load_data():
    """حمّل البيانات من ملف JSON"""
    if not os.path.exists(DATA_FILE):
        save_data(DEFAULT_DATA.copy())
        return DEFAULT_DATA.copy()
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def save_data(data):
    """احفظ البيانات إلى ملف JSON"""
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


# ====== إدارة الطلاب ======

def add_student(student_id, name, class_name):
    """أضف طالباً"""
    data = load_data()

    if student_id in data["students"]:
        return False, f"Student ID {student_id} already exists!"

    data["students"][student_id] = {
        "name": name,
        "class_name": class_name,
        "scores": {}
    }

    if class_name not in data["classes"]:
        data["classes"].append(class_name)

    save_data(data)
    return True, f"Added student: {name} ({student_id})"


def delete_student(student_id):
    """احذف طالباً"""
    data = load_data()

    if student_id not in data["students"]:
        return False, f"Student ID {student_id} does not exist!"

    name = data["students"][student_id]["name"]
    del data["students"][student_id]
    save_data(data)
    return True, f"Deleted student: {name}"


def get_student(student_id):
    """احصل على معلومات طالب واحد"""
    data = load_data()
    return data["students"].get(student_id)


def get_all_students():
    """احصل على جميع الطلاب"""
    data = load_data()
    return data["students"]


# ====== إدارة الدرجات ======

def add_score(student_id, subject, score):
    """أدخل درجة"""
    data = load_data()

    if student_id not in data["students"]:
        return False, f"Student ID {student_id} does not exist!"

    if subject not in data["subjects"]:
        return False, f"Subject {subject} does not exist!"

    if not isinstance(score, (int, float)) or score < 0 or score > 100:
        return False, "Score must be between 0 and 100!"

    data["students"][student_id]["scores"][subject] = score
    save_data(data)
    return True, f"Entered {data['students'][student_id]['name']}'s {subject} score: {score}"


def update_score(student_id, subject, score):
    """حدّث درجة (نفس وظيفة add_score)"""
    return add_score(student_id, subject, score)


def delete_score(student_id, subject):
    """احذف درجة لمادة"""
    data = load_data()

    if student_id not in data["students"]:
        return False, f"Student ID {student_id} does not exist!"

    if subject not in data["students"][student_id]["scores"]:
        return False, f"{data['students'][student_id]['name']} has no {subject} score"

    del data["students"][student_id]["scores"][subject]
    save_data(data)
    return True, f"Deleted {data['students'][student_id]['name']}'s {subject} score"


# ====== كود الاختبار ======
if __name__ == "__main__":
    print("=== Testing Data Layer ===")
    print(add_student("2024001", "Zhang San", "Class 1"))
    print(add_student("2024002", "Li Si", "Class 1"))
    print(add_student("2024003", "Wang Wu", "Class 2"))

    print(add_score("2024001", "Chinese", 85))
    print(add_score("2024001", "Math", 92))
    print(add_score("2024002", "Chinese", 90))

    student = get_student("2024001")
    print(f"\nZhang San's info: {student}")

    print(f"\nTotal students: {len(get_all_students())}")
86 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

الناتج:

TEXT 📖 للعرض فقط
=== Testing Data Layer ===

▶ مثال: عمليات بيانات الطلاب (الصعوبة ⭐)

PYTHON
students = {}

def add_student(students, sid, name, class_name):
    if sid in students:
        return f"Student ID {sid} already exists!"
    students[sid] = {"name": name, "class_name": class_name, "scores": {}}
    return f"Added: {name} ({sid})"

def add_score(students, sid, subject, score):
    if sid not in students:
        return f"Student ID {sid} does not exist!"
    if score < 0 or score > 100:
        return "Score must be between 0 and 100!"
    students[sid]["scores"][subject] = score
    return f"Entered {students[sid]['name']}'s {subject}: {score}"

def get_average(students, sid):
    if sid not in students or not students[sid]["scores"]:
        return None
    scores = students[sid]["scores"].values()
    return sum(scores) / len(scores)

print(add_student(students, "001", "Alice", "Class 1"))
print(add_student(students, "002", "Bob", "Class 1"))
print(add_score(students, "001", "Chinese", 85))
print(add_score(students, "001", "Math", 92))
print(add_score(students, "002", "Chinese", 78))

avg = get_average(students, "001")
print(f"\nAlice's average: {avg:.1f}")
print(f"Bob's Chinese score: {students['002']['scores']['Chinese']}")
print(f"Total students: {len(students)}")
▶ جرّب الكود

المخرجات:

TEXT 📖 للعرض فقط
Added: Alice (001)
Added: Bob (002)
Entered Alice's Chinese: 85
Entered Alice's Math: 92
Entered Bob's Chinese: 78

Alice's average: 88.5
Bob's Chinese score: 78
Total students: 2


5. أبرز ملامح التصميم



❓ أسئلة شائعة

س لماذا استخدام رقم الطالب كمفتاح قاموس بدلاً من الاسم؟
ج يمكن للأسماء أن تتكرر (طلاب بنفس الاسم)، لكن أرقام الطلاب معرفات فريدة. مفاتيح القاموس يجب أن تكون فريدة؛ استخدام أرقام الطلاب يضمن سرعة بحث O(1) وعدم وجود تعارضات.
س هل من الممارسات الجيدة ترميز مسار ملف JSON بشكل ثابت؟
ج للبساطة، الدورة ترمّز المسار. في المشاريع الحقيقية، استخدم ملفات تهيئة أو وسائط سطر أوامر لتحديد المسار. استخدم os.path.join() لبناء المسارات وتجنب الترميز الثابت.
س لماذا إرجاع صف (success, message) بدلاً من إثارة استثناءات؟
ج هذا هو "نمط رمز الحالة" — يمكن للمتصل أن يقرر ما يفعله بناءً على success، وهو أخف من try/except. مناسب لأدوات CLI الصغيرة؛ للمشاريع الأكبر، يُوصى بفئات استثناء مخصصة.

📖 ملخص


📝 تمارين

  1. مبتدئ (الصعوبة ⭐): شغّل كود طبقة البيانات أعلاه، أضف 2-3 طلاب بدرجاتهم، وتحقق من إنشاء ملف JSON بشكل صحيح.

  2. متوسط (الصعوبة ⭐⭐): أضف دالة update_student(student_id, name=None, class_name=None) إلى طبقة البيانات التي تُحدّث فقط الحقول المُقدّمة (تترك الأخرى دون تغيير).

  3. متقدم (الصعوبة ⭐⭐⭐): أضف وظيفة إدخال دفعة إلى add_score — اقبل قاموساً مثل {"Chinese": 85, "Math": 92} لإدخال درجات متعددة مرة واحدة. متطلب: إذا كانت أي درجة غير صالحة، تراجع عن عملية الدفعة بأكملها (لا تحفظ أي بيانات).

Web-Tutorial.com

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

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

100%