الفصل التاسع: معالجة الأخطاء والاستثناءات (Exception Handling)
مقدمة الفصل
تخيل أنك تبني روبوتًا يعمل على خط تجميع. فجأة، تصل قطعة بحجم خاطئ. ماذا يجب أن يفعل الروبوت؟ هل يجب أن يتوقف عن العمل تمامًا ويغلق المصنع بأكمله؟ هذا ما يفعله البرنامج عندما “ينهار” (crashes). أم هل يجب أن يكون لديه تعليمات مسبقة تقول: “حاول تركيب القطعة. باستثناء أنها إذا كانت بالحجم الخاطئ، فضعها جانبًا، وسجل ملاحظة، ثم أكمل عملك”؟
معالجة الاستثناءات هي تعليمات الطوارئ هذه. إنها تحول برامجك من آلات هشة إلى عمال أذكياء قادرين على التعامل مع المشاكل غير المتوقعة بأمان، بدلاً من الانهيار. هذه المهارة هي إحدى العلامات الفارقة التي تميز المبرمج المحترف عن المبتدئ.
1. ما هو الاستثناء؟ وأشهر أنواعه
الاستثناء هو حدث (خطأ) يعترض السير الطبيعي لبرنامجك. بايثون تحتوي على العديد من أنواع الاستثناءات المدمجة، ولكل منها اسم يدل على طبيعة الخطأ.
أشهر أنواع الاستثناءات التي ستواجهها:
اسم الاستثناء | سبب الحدوث | مثال |
---|---|---|
ValueError |
عندما تكون قيمة البيانات غير مناسبة (مثل محاولة تحويل نص ليس رقمًا إلى رقم). | int("abc") |
FileNotFoundError |
عند محاولة فتح ملف غير موجود للقراءة. | open("ghost.txt", "r") |
ZeroDivisionError |
عند محاولة قسمة أي عدد على صفر. | 10 / 0 |
TypeError |
عند محاولة إجراء عملية على أنواع بيانات غير متوافقة. | "Hello" + 5 |
IndexError |
عند محاولة الوصول إلى عنصر في قائمة باستخدام فهرس خارج النطاق. | my_list = [1,2]; print(my_list[2]) |
2. التقاط الأخطاء باستخدام try
و except
لتجنب توقف البرنامج، يمكننا وضع الكود الذي نشك أنه قد يسبب خطأً داخل كتلة try
. وإذا حدث خطأ بالفعل، فسيتم تنفيذ الكود الموجود داخل كتلة except
المرتبطة به.
البنية الأساسية:
try:
# الكود الذي قد يسبب خطأ
except:
# الكود الذي سينفذ في حال حدوث الخطأ
مثال عملي: لنتخيل أننا نطلب من المستخدم إدخال عمره. المستخدم قد يخطئ ويدخل نصًا بدلاً من رقم.
try:
# نحاول تحويل مدخلات المستخدم إلى عدد صحيح
age_str = input("أدخل عمرك: ")
age = int(age_str)
print(f"في العام القادم، سيكون عمرك {age + 1} عامًا.")
except:
# هذه الكتلة تعمل إذا فشلت عملية التحويل بـ int
print("إدخال غير صالح. الرجاء إدخال رقم صحيح لعمرك.")
شرح الكود بالتفصيل:
try:
: نضع الكود “الخطير” داخل هذه الكتلة. في هذه الحالة، السطرage = int(age_str)
هو مصدر الخطر، لأنه سيفشل إذا أدخل المستخدم نصًا مثل “عشرون”.except:
: إذا فشل أي سطر داخل كتلةtry
وأثار استثناءً، فإن بايثون تتوقف فورًا عن تنفيذ بقية كتلةtry
وتقفز مباشرة لتنفيذ الكود الموجود داخل كتلةexcept
.- بهذه الطريقة، بدلاً من أن يتوقف البرنامج ويعرض رسالة خطأ حمراء غير مفهومة للمستخدم، فإنه يعرض رسالة واضحة ولطيفة تخبر المستخدم بكيفية تصحيح خطئه، ثم يواصل عمله بشكل طبيعي (أو ينتهي بأمان).
3. التعامل مع أنواع محددة من الأخطاء
الكتلة except:
العامة تلتقط أي نوع من الأخطاء، لكن هذا ليس دائمًا السلوك المرغوب. من الأفضل تحديد نوع الخطأ الذي تتوقع حدوثه بالضبط. هذا يجعل كودك أكثر دقة ووضوحًا، ويمنع إخفاء أخطاء غير متوقعة.
مثال: آلة حاسبة آمنة
هذا البرنامج قد يواجه خطأين محتملين: ValueError
إذا أدخل المستخدم نصًا، و ZeroDivisionError
إذا حاول القسمة على صفر.
try:
# الحصول على رقمين من المستخدم
num1 = float(input("أدخل الرقم الأول: "))
num2 = float(input("أدخل الرقم الثاني: "))
# إجراء عملية القسمة
result = num1 / num2
print(f"النتيجة هي: {result}")
except ValueError:
# هذه الكتلة تعمل فقط إذا فشلت عملية التحويل بـ float
print("خطأ: الرجاء إدخال أرقام صالحة فقط.")
except ZeroDivisionError:
# هذه الكتلة تعمل فقط إذا حاول المستخدم القسمة على صفر
print("خطأ: لا يمكن القسمة على صفر.")
شرح الكود بالتفصيل:
يمكنك إضافة أي عدد من كتل except
بعد try
. عندما يحدث خطأ، سيبحث بايثون عن أول كتلة except
تطابق نوع الخطأ الذي حدث ويقوم بتنفيذها.
الحصول على تفاصيل الخطأ باستخدام as
في بعض الأحيان، قد ترغب في عرض رسالة الخطأ الأصلية التي توفرها بايثون. يمكنك “التقاط” كائن الاستثناء نفسه في متغير باستخدام الكلمة المفتاحية as
.
try:
result = 10 / 0
except ZeroDivisionError as e:
# 'e' الآن يحتوي على كائن الاستثناء ورسالته
print("حدث خطأ ما!")
print(f"تفاصيل الخطأ من بايثون: {e}")
💡 نصيحة احترافية: إذا كنت تريد تنفيذ نفس الكود لعدة أنواع مختلفة من الأخطاء، يمكنك جمعها في
tuple
واحد.try: # كود قد يسبب ValueError أو TypeError except (ValueError, TypeError) as e: print(f"حدث خطأ في نوع أو قيمة البيانات: {e}")
4. استخدام else
و finally
يمكن توسيع بنية try...except
لتشمل كتلتين إضافيتين اختياريتين: else
و finally
. لفهمهما، تخيل مسارين يمكن أن يسلكهما برنامجك:
-
مسار النجاح: يتم تنفيذ
try
بنجاح -> يتم تنفيذelse
-> يتم تنفيذfinally
. -
مسار الفشل: يفشل
try
-> يتم تنفيذexcept
المطابق -> يتم تنفيذfinally
.else
: الكود الموجود داخل هذه الكتلة يتم تنفيذه فقط إذا لم يحدث أي خطأ في كتلةtry
.finally
: الكود الموجود داخل هذه الكتلة يتم تنفيذه دائمًا، سواء حدث خطأ أم لا. غالبًا ما تُستخدم لعمليات “التنظيف”، مثل إغلاق ملف أو اتصال بقاعدة بيانات.
try:
# محاولة قراءة رقم
num = int(input("أدخل رقمًا: "))
except ValueError:
# يعمل فقط في حال حدوث ValueError
print("هذا لم يكن رقمًا صالحًا.")
else:
# يعمل فقط في حال عدم حدوث أي استثناء
print(f"لقد أدخلت الرقم {num}.")
finally:
# يعمل دائمًا، بغض النظر عما حدث
print("انتهت المحاولة.")
5. إثارة الاستثناءات عمدًا باستخدام raise
حتى الآن، كنا نتعامل مع الأخطاء التي تثيرها بايثون تلقائيًا. لكن يمكنك أيضًا إثارة استثناء بنفسك باستخدام الكلمة المفتاحية raise
. هذا مفيد جدًا داخل الدوال للتحقق من صحة المدخلات وفرض قواعد معينة.
مثال: لنكتب دالة تحسب الخصم، لكنها ترفض أي نسبة خصم غير منطقية.
def calculate_discount(price, discount_percentage):
"""تحسب قيمة الخصم، ولكن تثير خطأ إذا كانت النسبة غير صالحة."""
# التحقق من أن نسبة الخصم بين 0 و 1 (مثلاً 0.2 لـ 20%)
if not 0 <= discount_percentage <= 1:
# إذا لم تكن كذلك، أثر استثناءً بنفسك مع رسالة واضحة
raise ValueError("نسبة الخصم يجب أن تكون بين 0 و 1")
return price * discount_percentage
# يمكننا الآن استخدام try/except لمعالجة الخطأ الذي أنشأناه
try:
discount = calculate_discount(100, 5) # محاولة استخدام قيمة خاطئة
print(f"قيمة الخصم: {discount}")
except ValueError as e:
print(f"خطأ في الإدخال: {e}")
الفائدة: raise
تجعل دوالك أكثر قوة وموثوقية، حيث ترفض القيم غير الصحيحة بشكل واضح وصريح بدلاً من إعطاء نتائج خاطئة بصمت.
6. تمرين تطبيقي: تحسين برنامج دفتر الملاحظات
المطلوب:
في الفصل السابق، قمنا ببناء برنامج بسيط لدفتر الملاحظات. الآن، سنقوم بتحسينه ليكون أكثر قوة. سنضيف خيارًا جديدًا وهو “عرض الملاحظات”. يجب أن يتعامل هذا الخيار مع الحالة التي يحاول فيها المستخدم عرض الملاحظات ولكن الملف notes.txt
غير موجود بعد.
الحل المقترح:
# --- برنامج دفتر الملاحظات المحسن ---
while True:
print("\n--- دفتري ---")
print("1. إضافة ملاحظة جديدة")
print("2. عرض كل الملاحظات")
print("3. الخروج")
choice = input("أدخل اختيارك: ")
if choice == '1':
note_text = input("اكتب ملاحظتك: ")
# نستخدم 'a' للإضافة إلى نهاية الملف
with open("notes.txt", "a", encoding="utf-8") as f:
f.write(note_text + "\n")
print("تم حفظ ملاحظتك بنجاح!")
elif choice == '2':
# نضع الكود الذي قد يسبب خطأ داخل كتلة try
try:
with open("notes.txt", "r", encoding="utf-8") as f:
notes = f.read()
print("\n--- ملاحظاتك ---")
# نستخدم .strip() للتأكد أن الملف ليس فارغًا أو يحتوي على مسافات فقط
if notes.strip():
print(notes)
else:
print("ملف الملاحظات فارغ.")
# نلتقط الخطأ المحدد وهو عدم وجود الملف
except FileNotFoundError:
print("\nلا توجد ملاحظات لعرضها. حاول إضافة ملاحظة أولاً.")
elif choice == '3':
print("إلى اللقاء!")
break
else:
print("اختيار غير صالح. الرجاء المحاولة مرة أخرى.")
شرح الحل المحسن:
- أضفنا كتلة
elif choice == '2':
للتعامل مع خيار المستخدم الجديد. - وضعنا الكود المسؤول عن قراءة الملف (
with open(...)
) داخل كتلةtry
. هذا هو الكود الذي قد يفشل إذا لم يكن الملف موجودًا. - أضفنا
except FileNotFoundError:
. هذه الكتلة سيتم تنفيذها فقط إذا كان نوع الخطأ هوFileNotFoundError
، مما يسمح لنا بعرض رسالة مفيدة للمستخدم ترشده إلى ما يجب عليه فعله، بدلاً من انهيار البرنامج.
7. خلاصة الفصل
- معالجة الاستثناءات هي تقنية أساسية لكتابة برامج موثوقة لا تتوقف عن العمل بشكل مفاجئ.
- استخدم
try
لوضع الكود الذي قد يسبب خطأ. - استخدم
except
لتحديد ما يجب فعله عند حدوث خطأ. من الأفضل دائمًا تحديد نوع الخطأ الذي تتوقعه والتقاط معلوماته باستخدامas
. - يمكنك إثارة أخطائك الخاصة باستخدام
raise
لفرض قواعد معينة في برنامجك. - استخدم
else
لتنفيذ كود فقط في حالة عدم حدوث أخطاء. - استخدم
finally
لتنفيذ كود “تنظيف” سيتم تشغيله دائمًا.
تمارين تطبيقية مع الحلول
التمرين 1: حاسبة العمر الآمنة
المطلوب:
اكتب برنامجًا يسأل المستخدم عن سنة ميلاده، ثم يحسب عمره التقريبي (بافتراض أن العام الحالي 2025). يجب أن يستخدم البرنامج كتلة try...except
للتعامل مع الحالة التي يدخل فيها المستخدم نصًا بدلاً من رقم، ويطبع رسالة خطأ واضحة في هذه الحالة.
الحل:
# --- برنامج حاسبة العمر الآمنة ---
CURRENT_YEAR = 2025
try:
# 1. محاولة الحصول على المدخلات وتحويلها إلى رقم صحيح
birth_year_str = input("أدخل سنة ميلادك: ")
birth_year = int(birth_year_str)
# 2. حساب العمر
age = CURRENT_YEAR - birth_year
print(f"عمرك التقريبي هو {age} عامًا.")
except ValueError:
# 3. هذه الكتلة تعمل فقط إذا فشلت عملية int()
print("خطأ: الرجاء إدخال سنة ميلادك كأرقام صحيحة فقط.")
التمرين 2: الوصول الآمن إلى القائمة
المطلوب:
لديك قائمة my_list = ["تفاح", "موز", "برتقال"]
. اكتب برنامجًا يطلب من المستخدم إدخال “فهرس” (index) ويحاول طباعة العنصر الموجود في هذا الفهرس. يجب معالجة خطأ IndexError
الذي يحدث إذا أدخل المستخدم فهرسًا خارج نطاق القائمة.
الحل:
# --- برنامج الوصول الآمن للقائمة ---
my_list = ["تفاح", "موز", "برتقال"]
print(f"القائمة الحالية: {my_list}")
try:
# محاولة الحصول على الفهرس من المستخدم وتحويله إلى رقم
index_str = input("أدخل الفهرس الذي تريد عرضه (0, 1, 2): ")
index = int(index_str)
# محاولة الوصول إلى العنصر وطباعته
print(f"العنصر في الفهرس {index} هو: {my_list[index]}")
except IndexError:
# تعمل هذه الكتلة إذا كان الفهرس خارج النطاق (أكبر من 2 أو أصغر من -3)
print(f"خطأ: الفهرس {index} غير موجود في القائمة.")
except ValueError:
# معالجة إضافية إذا أدخل المستخدم نصًا بدلاً من رقم للفهرس
print("خطأ: الرجاء إدخال رقم صحيح للفهرس.")
التمرين 3: استخدام كتلة else
المطلوب:
اكتب برنامجًا يطلب من المستخدم رقمين ويقسمهما. إذا تمت عملية القسمة بنجاح (بدون أي أخطاء)، يجب أن تطبع كتلة else
رسالة “تمت العملية الحسابية بنجاح.” إذا حدث أي خطأ، يجب أن تطبع كتلة except
رسالة “حدث خطأ ما.”
الحل:
# --- برنامج يوضح استخدام else ---
try:
print("--- آلة القسمة ---")
num1 = float(input("أدخل الرقم المقسوم: "))
num2 = float(input("أدخل الرقم المقسوم عليه: "))
result = num1 / num2
print(f"النتيجة هي: {result}")
except:
# تعمل هذه الكتلة عند حدوث أي خطأ (ValueError, ZeroDivisionError, etc.)
print("حدث خطأ ما أثناء العملية.")
else:
# هذه الكتلة تعمل فقط إذا لم تحدث أي أخطاء في كتلة try
print("تمت العملية الحسابية بنجاح.")
التمرين 4: استخدام كتلة finally
المطلوب:
اكتب برنامجًا يحاول قراءة ملف. بغض النظر عما إذا كانت عملية القراءة ناجحة أو فشلت (بسبب عدم وجود الملف)، يجب أن تطبع كتلة finally
دائمًا رسالة “انتهاء محاولة قراءة الملف.”
الحل:
# --- برنامج يوضح استخدام finally ---
try:
print("محاولة فتح ملف 'data.txt'...")
# افترض أن هذا الملف قد يكون موجودًا أو لا
with open("data.txt", "r", encoding="utf-8") as file:
content = file.read()
print("تمت قراءة الملف بنجاح.")
except FileNotFoundError:
print("خطأ: الملف 'data.txt' غير موجود.")
finally:
# هذه الكتلة ستعمل دائمًا، سواء تم العثور على الملف أم لا
print("...انتهاء محاولة قراءة الملف.")
التمرين 5: عرض تفاصيل الخطأ
المطلوب:
اكتب دالة تحاول قسمة رقمين. استخدم except ... as e
لالتقاط كائن الاستثناء وطباعة رسالة الخطأ الافتراضية التي توفرها بايثون. قم بتجربة الدالة مع حالة قسمة على صفر.
الحل:
def divide_numbers(numerator, denominator):
"""تقوم بقسمة رقمين وتعالج خطأ القسمة على صفر."""
try:
result = numerator / denominator
print(f"النتيجة هي: {result}")
except ZeroDivisionError as e:
print("لا يمكنك القسمة على صفر!")
print(f"رسالة الخطأ من بايثون: '{e}'")
# تجربة الدالة
divide_numbers(10, 2) # ستنجح
print("---")
divide_numbers(10, 0) # ستفشل وستعرض رسالة الخطأ
التمرين 6: معالجة أخطاء متعددة
المطلوب:
اكتب برنامجًا يحتوي على قائمة من الأرقام data = [100]
. يطلب البرنامج من المستخدم إدخال رقم ليقسم عليه العنصر الأول في القائمة. يجب على البرنامج معالجة ثلاثة أخطاء محتملة برسائل مختلفة: ValueError
، ZeroDivisionError
، و IndexError
(في حال كانت القائمة فارغة).
الحل:
# --- برنامج معالجة أخطاء متعددة ---
data = [100]
try:
divisor_str = input("أدخل رقمًا لتقسم عليه العدد 100: ")
divisor = int(divisor_str)
result = data[0] / divisor # قد يسبب IndexError أو ZeroDivisionError
print(f"النتيجة: {result}")
except ValueError:
print("خطأ إدخال: الرجاء إدخال رقم صالح.")
except ZeroDivisionError:
print("خطأ رياضي: لا يمكن القسمة على صفر.")
except IndexError:
print("خطأ في البيانات: القائمة فارغة ولا يمكن الوصول للعنصر الأول.")
التمرين 7: إثارة خطأ مخصص (raise
)
المطلوب:
اكتب دالة register_user(age)
تأخذ عمر المستخدم كمعامل. إذا كان العمر أقل من 18، يجب على الدالة أن تثير استثناء ValueError
مع رسالة “يجب أن يكون عمر المستخدم 18 عامًا على الأقل.” وإلا، تطبع “تم تسجيل المستخدم بنجاح.”
الحل:
def register_user(age):
"""تسجل المستخدم إذا كان عمره 18 أو أكبر، وإلا تثير خطأ."""
if age < 18:
# إثارة استثناء بشكل متعمد إذا لم يتحقق الشرط
raise ValueError("يجب أن يكون عمر المستخدم 18 عامًا على الأقل.")
else:
print("تم تسجيل المستخدم بنجاح.")
# تجربة الدالة مع try/except
try:
register_user(25) # ستنجح
register_user(16) # ستثير خطأ
except ValueError as e:
print(f"فشل التسجيل. السبب: {e}")
التمرين 8: معالجة عدة استثناءات في كتلة واحدة
المطلوب:
اكتب دالة تأخذ وسيطين وتحاول ضربهما. قد يحدث خطأ TypeError
(إذا حاولنا ضرب نص مع رقم). اكتب كتلة except
واحدة تلتقط كلاً من TypeError
و ValueError
وتطبع رسالة خطأ عامة.
الحل:
def multiply_safely(a, b):
"""تحاول ضرب قيمتين وتعالج أخطاء النوع والقيمة."""
try:
result = a * b
print(f"نتيجة الضرب: {result}")
except (TypeError, ValueError):
# هذه الكتلة تلتقط أيًا من الخطأين
print("خطأ: المدخلات غير صالحة لعملية الضرب.")
# تجربة الدالة
multiply_safely(10, 5) # ستنجح
multiply_safely("hello", 3) # ستنجح (تكرار النص)
multiply_safely("hello", "world") # ستفشل
multiply_safely(10, "world") # ستفشل
التمرين 9: التحقق من وجود ملف قبل القراءة
المطلوب: اكتب برنامجًا يطلب من المستخدم اسم ملف ليقرأه. يجب على البرنامج أن يتحقق أولاً مما إذا كان الملف موجودًا أم لا.
- إذا كان موجودًا، يقرأ محتواه ويطبعه.
- إذا لم يكن موجودًا، يجب أن يلتقط خطأ
FileNotFoundError
ويطبع رسالة مناسبة.
الحل:
# --- برنامج القراءة الآمنة للملفات ---
filename = input("أدخل اسم الملف الذي تريد قراءته (e.g., notes.txt): ")
try:
with open(filename, "r", encoding="utf-8") as file:
content = file.read()
print(f"\n--- محتويات ملف '{filename}' ---")
print(content)
except FileNotFoundError:
print(f"خطأ: الملف '{filename}' غير موجود.")
التمرين 10: برنامج كامل مع try...except...else...finally
المطلوب: اكتب برنامجًا يطلب من المستخدم اسم ملف يحتوي على أرقام.
try
: حاول فتح الملف وقراءة محتواه وتحويله إلى قائمة من الأرقام.except FileNotFoundError
: تعامل مع حالة عدم وجود الملف.except ValueError
: تعامل مع حالة وجود نص غير رقمي في الملف.else
: إذا نجحت كل العمليات، اطبع مجموع الأرقام.finally
: في جميع الحالات، اطبع “انتهت عملية معالجة الملف.”
الحل:
# --- برنامج معالجة ملف الأرقام الكامل ---
# للتجربة، أنشئ ملفًا اسمه "numbers.txt" وضع فيه أرقامًا على أسطر منفصلة.
filename = "numbers.txt"
numbers = []
try:
print(f"محاولة فتح وقراءة الملف '{filename}'...")
with open(filename, "r", encoding="utf-8") as file:
for line in file:
numbers.append(int(line.strip()))
except FileNotFoundError:
print("خطأ: الملف غير موجود.")
except ValueError:
print("خطأ: الملف يحتوي على بيانات غير رقمية لا يمكن تحويلها.")
else:
# تعمل هذه الكتلة فقط إذا لم تحدث أخطاء
total = sum(numbers)
print(f"تمت قراءة الأرقام بنجاح. المجموع هو: {total}")
finally:
# تعمل هذه الكتلة دائمًا
print("...انتهت عملية معالجة الملف.")
ماذا بعد؟
الآن بعد أن تعلمت كيفية بناء برامج قوية ومنظمة، حان الوقت للانتقال إلى أحد أقوى نماذج البرمجة وأكثرها استخدامًا. في الفصل العاشر، سنغوص في عالم البرمجة كائنية التوجه (Object-Oriented Programming - OOP)، حيث ستتعلم كيفية التفكير في برامجك كمجموعة من الكائنات المتفاعلة.