معالجة الاستثناءات

تحدث دائماً مواقف غير متوقعة أثناء تنفيذ البرنامج — ملفات غير موجودة، انقطاع الشبكة، إدخال المستخدم لبيانات غير صالحة... بدون معالجة هذه "الاستثناءات"، ينهار البرنامج. معالجة الاستثناءات تسمح لبرنامجك بالتعافي من الأخطاء وتقديم رسائل سهلة للمستخدم بدلاً من رسائل الخطأ المشفرة.


1. ما هو الاستثناء

الاستثناء هو خطأ يحدث أثناء تنفيذ البرنامج. إذا لم يتم معالجة الاستثناء، ينهي البرنامج تشغيله:

PYTHON
# هذا الكود سينهار
# print(10 / 0)               # ZeroDivisionError: division by zero
# int("abc")                  # ValueError: invalid literal for int()
# open("non_existent.txt")    # FileNotFoundError

استخدم try-except لمعالجة هذه الأخطاء بشكل أنيق:

مثال: try-except لمعالجة القسمة وأخطاء الإدخال

PYTHON
try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print(f"10 / {number} = {result}")
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
▶ جرّب الكود

تشغيل نموذجي:

TEXT
Enter a number: abc
That's not a valid number!
💡 فلسفة معالجة الاستثناءات: بدلاً من استخدام try لـ "التنبؤ بجميع الأخطاء المحتملة"، يتعلق الأمر بـ "فعل الأشياء أولاً والتعامل مع المشكلات عند ظهورها." مجتمع بايثون يسميها: "من الأسهل طلب المغفرة من الإذن" (EAFP).


2. هيكل try-except الكامل

مثال: هيكل try-except-else-finally الكامل

PYTHON
try:
    # كود قد يسبب خطأ
    file = open("data.txt", "r", encoding="utf-8")
    content = file.read()
    number = int(content)
    print(f"The number is: {number}")
except FileNotFoundError:
    print("File not found!")
except ValueError:
    print("File content is not a valid number!")
else:
    # يُنفّذ عندما لا يحدث استثناء
    print("Successfully read and converted!")
finally:
    # يُنفّذ دائماً، بغض النظر عن الاستثناءات
    print("Cleanup: closing file")
    try:
        file.close()
    except NameError:
        pass
▶ جرّب الكود
الجزء متى يُنفّذ الغرض
try دائماً ضع الكود الذي قد يسبب خطأ هنا
except عند حدوث الاستثناء المحدد تعامل مع الخطأ
else عندما لا يحدث استثناء منطق بعد النجاح
finally دائماً (مع أو بدون استثناء) تنظيف الموارد (إغلاق الملفات/تحرير الاتصالات)
💡 في التطوير الحقيقي، else يُستخدم أقل، بينما finally يُستخدم بكثرة. جملة with (مدير السياق) تتعامل مع تحرير الموارد تلقائياً ويمكنها غالباً استبدال finally.


3. أنواع الاستثناءات الشائعة

PYTHON
# ZeroDivisionError — القسمة على صفر
# int("abc")                # ValueError — قيمة غير صالحة
# [1, 2][5]                 # IndexError — فهرس خارج النطاق
# {"a": 1}["b"]             # KeyError — مفتاح غير موجود
# open("x.txt")             # FileNotFoundError — ملف غير موجود
# 10 + "hello"              # TypeError — خطأ في النوع
# import nonexistent        # ModuleNotFoundError — وحدة غير موجودة

التقاط جميع الاستثناءات (غير موصى به)

مثال: الطريقة الصحيحة مقابل الطريقة الخاطئة لالتقاط الاستثناءات

PYTHON
try:
    # بعض الكود
    pass
except:                     # يلتقط جميع الاستثناءات
    print("Something went wrong")

try:
    pass
except Exception as e:      # يلتقط الاستثناءات المعروفة
    print(f"Error: {e}")
▶ جرّب الكود
⚠️ استخدام except: بمفرده غير موصى به — يلتقط استثناءات النظام مثل KeyboardInterrupt (Ctrl+C) و SystemExit، مما يمنع إنهاء البرنامج بشكل طبيعي. استخدم except Exception as e بدلاً من ذلك. أفضل ممارسة هي التقاط أنواع الاستثناءات المحددة التي تتوقعها فقط.


4. raise: إثارة الاستثناءات صراحة

أحياناً تحتاج لإخبار المتصل "حدث خطأ ما." استخدم raise:

مثال: raise لإثارة استثناء

PYTHON
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero!")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(f"Error: {e}")     # Error: Cannot divide by zero!
▶ جرّب الكود

مثال: raise للتحقق من كلمة المرور

PYTHON
def validate_password(password):
    """تحقق من قوة كلمة المرور؛ أثر استثناءً إذا لم تتحقق الشروط"""
    if len(password) < 6:
        raise ValueError("Password must be at least 6 characters")
    if not any(c.isdigit() for c in password):
        raise ValueError("Password must contain a digit")
    if not any(c.isalpha() for c in password):
        raise ValueError("Password must contain a letter")
    return True

try:
    validate_password("123")
except ValueError as e:
    print(f"Invalid password: {e}")
▶ جرّب الكود
💡 متى تستخدم raise؟ عندما تواجه الدالة موقفاً "لا ينبغي أن يحدث" — قيم معاملات غير صالحة، شروط مسبقة غير محققة، تبعيات غير متوفرة. أثر استثناءً ليقرر المتصل كيفية التعامل معه، بدلاً من ابتلاع الخطأ بصمت داخل الدالة.


5. فئات الاستثناء المخصصة

يمكنك إنشاء أنواع الاستثناءات الخاصة بك للحصول على معلومات خطأ أكثر دقة:

مثال: استثناءات مخصصة للسحب البنكي

PYTHON
class BalanceError(Exception):
    """استثناء رصيد غير كافٍ"""
    pass

class AccountLockedError(Exception):
    """استثناء حساب مقفل"""
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise BalanceError(f"Insufficient balance! Need {amount}, have {balance}")
    if balance < 0:
        raise AccountLockedError("Account is locked")
    return balance - amount

try:
    withdraw(100, 200)
except BalanceError as e:
    print(f"Transaction failed: {e}")
except AccountLockedError as e:
    print(f"Transaction failed: {e}")
▶ جرّب الكود
💡 الاستثناءات المخصصة تجعل كودك أكثر وضوحاً — يمكن للمتصلين معالجة أنواع مختلفة من الأخطاء بدقة. ببساطة ارث من Exception؛ عادة اسم فئة فقط كافٍ، لا حاجة لطرق إضافية.


6. logging: تسجيل الأخطاء باحترافية

استخدام print() لرسائل الخطأ يعمل في البرامج النصية البسيطة، لكن في المشاريع الإنتاجية، استخدم وحدة logging:

مثال: تهيئة واستخدام logging

PYTHON
import logging

# تهيئة أساسية
logging.basicConfig(
    level=logging.INFO,          # مستوى التسجيل
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),  # اكتب إلى ملف
        logging.StreamHandler()                            # أيضاً أخرج إلى وحدة التحكم
    ]
)

# مستويات تسجيل مختلفة
logging.debug("Debug info")        # تصحيح، عادة لا يُخرج
logging.info("Application started successfully")     # معلومات عامة
logging.warning("Disk usage at 85%")  # تحذير
logging.error("Database connection failed")   # خطأ
logging.critical("System crash!")    # خطأ فادح
▶ جرّب الكود

المخرجات (تُكتب إلى كل من الملف ووحدة التحكم):

TEXT
2026-06-23 16:00:00,123 [INFO] Application started successfully
2026-06-23 16:00:00,124 [WARNING] Disk usage at 85%
2026-06-23 16:00:00,124 [ERROR] Database connection failed
💡 فوائد logging على print(): 1) يمكن الإخراج إلى كل من وحدة التحكم والملف في وقت واحد. 2) لديه تحكم في المستوى (أظهر كل شيء أثناء التطوير، فقط WARNING+ في الإنتاج). 3) يتضمن تلقائياً الطوابع الزمنية. 4) لا حاجة لحذف مجموعة من جمل print() قبل الإصدار.


حالات الاستخدام الشائعة


❓ أسئلة شائعة

س ماذا يعني as e في except Exception as e؟
ج as e يُسند كائن الاستثناء الملتقط إلى المتغير e، مما يسمح لك بفحص تفاصيل الاستثناء — print(e) يُظهر وصف الخطأ، type(e).__name__ يُظهر اسم نوع الاستثناء. بدون as e، لا يزال بإمكانك التقاط الاستثناء لكن لا يمكنك رؤية معلومات الخطأ التفصيلية. س:متى يجب استخدام الاستثناءات مقابل فحوصات if؟ ج:استخدم if للأخطاء التي يمكنك توقعها مسبقاً (مثل التحقق من القسمة على صفر). استخدم try-except للأخطاء التي لا يمكنك توقعها (مثل طلبات الشبكة، عمليات الملفات). أسلوب EAFP في مجتمع بايثون يميل نحو استخدام try — لكن هذه مسألة ذوق، وكلا النهجين لهما حالات استخدامهما. س:هل الكود في finally يُنفّذ دائماً؟ ج:دائماً تقريباً — حتى إذا كانت كتلة try تحتوي على return أو break أو continue، لا يزال finally يُنفّذ. الاستثناء الوحيد هو عندما يُنهي البرنامج قسراً (مثل os._exit() أو انقطاع الكهرباء). هذا يجعل finally مكاناً ممتازاً لتنظيف الموارد.

📖 ملخص

  • الاستثناءات هي أخطاء وقت التشغيل؛ تعامل معها باستخدام try-except
  • الهيكل: tryexcept (حسب نوع الاستثناء المحدد) ← else (لا استثناء) ← finally (يُنفّذ دائماً)
  • الاستثناءات الشائعة: ValueError, TypeError, FileNotFoundError, ZeroDivisionError, IndexError, KeyError
  • raise يثير الاستثناءات صراحة، تاركاً للمتصل معالجة الأخطاء
  • فئات الاستثناء المخصصة ترث من Exception، تميز بدقة أنواع الأخطاء
  • وحدة logging تحل محل print()، تدعم التحكم في المستوى، الإخراج إلى ملف، والطوابع الزمنية

📝 تمارين

  1. مبتدئ (الصعوبة ⭐): اكتب برنامجاً يطلب من المستخدم رقمين ويحسب قسمتهما. استخدم try-except لمعالجة ValueError (الإدخال ليس رقماً) و ZeroDivisionError (القسمة على صفر)، مع رسالة خطأ ودية لكل منهما.

  2. متوسط (الصعوبة ⭐⭐): اكتب دالة read_int_from_file(filename) تقرأ الرقم الأول من ملف وتُرجعه. استخدم try-except لمعالجة: ملف غير موجود، محتوى الملف ليس رقماً صالحاً، ملف فارغ. أعد رسالة خطأ مختلفة لكل حالة.

  3. متقدم (الصعوبة ⭐⭐⭐): اكتب برنامج "آلة حاسبة آمنة." يُدخل المستخدم تعبيراً (مثل 10 / 3)، ويحسب البرنامج ويُخرج النتيجة. المتطلبات: 1) استخدم try-except لمعالجة القسمة على صفر، أخطاء القيمة، وأخطاء النوع. 2) استخدم logging لتسجيل كل عملية حساب (INFO) وخطأ (ERROR) إلى ملف calculator.log. 3) ادعم exit للخروج. تلميح: استخدم eval() لتقييم التعبير (لاحظ مخاوف الأمان — مقبول لهذا التمرين).

100%