استثناءات مخصصة
Custom Exceptions
استثناءات مخصصة — Custom Exceptions
الاستثناءات المدمجة في Python مثل ValueError وTypeError ممتازة للأخطاء العامة. لكن تطبيقك يملك منطقاً خاصاً — يحتاج أخطاء خاصة به. هل بريد المستخدم غير صالح؟ هل كلمة المرور ضعيفة جداً؟ هل الحساب البنكي ليس لديه رصيد كافٍ؟
ValueError("رصيد غير كافٍ") ينقل المعلومة، لكنه يعني أن المستدعي لا يستطيع التمييز بين “رصيد غير كافٍ” و"اسم غير صالح" إلا بقراءة نص الرسالة — وهذا هشّ. الحل: استثناءات مخصصة تعطي لكل حالة خطأ في تطبيقك اسماً واضحاً ونوعاً قابلاً للمقارنة.
أبسط استثناء مخصص
الاستثناءات في Python مجرد كلاسات ترث من Exception. أبسط شكل:
class InsufficientFundsError(Exception):
pass # لا نحتاج تعريف أي شيء — No need to define anything
هذا كافٍ تماماً. يمكنك إطلاقه والتقاطه:
raise InsufficientFundsError("الرصيد غير كافٍ للإتمام العملية")
try:
withdraw(account, 5000)
except InsufficientFundsError as e:
print(f"فشل السحب: {e}")
ورث InsufficientFundsError من Exception يعني أنه يُلتقط أيضاً بـ except Exception — كل الاستثناءات المخصصة تنتمي لعائلة Exception.
استثناء مع سياق إضافي
الميزة الحقيقية للاستثناءات المخصصة هي أنها كلاسات كاملة — يمكنها حمل بيانات:
لاحظ: المستدعي يمكنه الوصول لـ e.balance وe.shortfall مباشرة — يستطيع عرضها في واجهة المستخدم، تسجيلها في log، أو استخدامها لاتخاذ قرار. ValueError("رصيد غير كافٍ") لا يوفر هذا.
التسلسل الهرمي — Exception Hierarchy
عندما يكبر التطبيق، تحتاج تصنيف الأخطاء. المثال الكلاسيكي هو أخطاء التحقق من البيانات:
التسلسل الهرمي يعطيك مرونة في الالتقاط: except EmailError يلتقط أخطاء البريد فقط، except ValidationError يلتقط كل أخطاء التحقق بغض النظر عن الحقل. المستدعي يختار مستوى التفصيل الذي يحتاجه.
متى تعرّف استثناء مخصصاً؟
لا تعرّف استثناءً مخصصاً لكل شيء — الاستثناءات المدمجة قوية وكافية في كثير من الحالات. عرّف استثناءً مخصصاً عندما:
- المستدعي يحتاج التمييز برمجياً: إذا كان الكود الأعلى يريد فعل شيء مختلف بناءً على نوع الخطأ (مثل: عرض رسالة مختلفة، إعادة المحاولة، إرسال إشعار)
- الخطأ يحمل بيانات إضافية: إذا كان الخطأ يحتاج حقل الاسم، القيمة المُرسلة، أو أي سياق آخر لا تحمله رسالة النص وحدها
- المكتبة أو الوحدة تملك منطق خاص: وحدة
paymentsيجب أن تُطلقPaymentError، وليسValueErrorعاماً — المستدعي يعرف أن الخطأ من طبقة الدفع
لا تعرّف استثناءً مخصصاً إذا كانت استثناءات Python المدمجة تصف الحالة بدقة.
تسلسل الاستثناءات — raise from
أحياناً تلتقط استثناءً من مكتبة خارجية وتريد إطلاق استثناء خاص بتطبيقك — لكن مع الحفاظ على الاستثناء الأصلي للتصحيح (debugging):
raise UserNotFoundError(user_id) from original_error يفعل شيئين:
- يُطلق
UserNotFoundError— الاستثناء الذي يفهمه تطبيقنا - يحفظ
original_errorفي خاصية__cause__— متاح للتصحيح
عندما يطبع Python الـ traceback، يظهر كلا الاستثناءين مع عبارة “The above exception was the direct cause of the following exception” — مما يجعل تتبع المشكلة سهلاً.
raise from None — إخفاء السبب الأصلي
أحياناً السبب الأصلي تفصيل داخلي لا تريد كشفه للخارج:
try:
db.query(sql)
except sqlite3.Error as e:
# إخفاء تفاصيل قاعدة البيانات الداخلية — Hide internal DB details
raise DatabaseError("فشل استعلام قاعدة البيانات") from None
from None يقطع سلسلة الاستثناءات — لن يظهر الاستثناء الأصلي في traceback.
تحدي: استثناء مخصص بسياق
خلاصة
الاستثناءات المخصصة تحوّل رسائل الخطأ من نصوص مبهمة إلى أنواع قابلة للمقارنة تحمل سياقاً. عرّف استثناءً مخصصاً عندما يحتاج المستدعي التمييز أو عندما تحمل بيانات لا تحملها رسالة نصية وحدها. نظّمها في تسلسل هرمي عندما تتعدد الأنواع. استخدم raise X from Y للحفاظ على سلسلة الاستثناءات للتصحيح. في الدرس التالي ستطبق كل هذا في مختبر تحقق كامل من بيانات التسجيل.