2.26. إطلاق الأخطاء

يمكن للدالة أن تُبلّغ مستدعيها عن مشكلة عبر إطلاق استثناء. والكلمة المفتاحية هي raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

استدعاء square_root(-1) يتوقف عند سطر raise، ويقفز خارج الدالة، ويبحث عن except مطابق في المستدعي. وإذا لم يلتقطه أي مستدعٍ، ينتهي البرنامج النصي بأثر تتبّع (traceback).

2.26.1. لماذا الإطلاق بدلًا من إرجاع قيمة دلالية

طريقتان للإبلاغ عن "إدخال خاطئ":

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

صيغة الاستثناء عادةً أفضل:

  • على المستدعي أن يعالج حالة الخطأ عمدًا -- إما بـ try، أو بالسماح للاستثناء بالانتشار. أما القيم الدلالية فمن السهل نسيانها ومن السهل الخلط بينها وبين نتيجة عادية.

  • تنتقل رسالة الخطأ مع الاستثناء؛ بينما يتعيّن على نهج القيمة الدلالية إرفاق المعلومة التشخيصية في مكان آخر.

  • السلوك الافتراضي عند استثناء غير معالَج هو انهيار صاخب مع أثر تتبّع يشير إلى الاستدعاء المخالف. أما إرجاع None بصمت فيتحوّل إلى أخطاء خفيّة لاحقًا.

استخدم القيم الدلالية فقط عندما يكون "غير موجود" نتيجةً روتينيةً غير استثنائية -- فـ dict.get() تُرجع None عند مفتاح مفقود تحديدًا لأن من المتوقع أن تخفق عمليات البحث أحيانًا.

2.26.2. أصناف الاستثناءات المخصّصة

لإطلاق مشكلة قد يرغب المستدعي في تمييزها عن الأخطاء المدمجة، عرّف صنفًا فرعيًا من Exception:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

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

2.26.2.1. إعادة الإطلاق

إن raise مجرّدة داخل كتلة except تعيد إطلاق الاستثناء الحالي لينتشر إلى المعالج التالي:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

هذا هو الشكل الصحيح عندما ترغب دالة في ملاحظة خطأ (تسجيله، أو عدّه، أو التراجع عن تغيير جزئي) دون أن تعالجه فعلًا.

2.26.3. متى تلتقط ومتى تترك الانتشار

قاعدة عامة مفيدة:

  • التقط الاستثناء عند المستوى الذي يستطيع التعافي بشكل ذي معنى -- باستبدال قيمة افتراضية، أو إعادة المحاولة، أو تخطّي الإدخال الخاطئ.

  • اترك الاستثناء ينتشر عندما لا يكون هناك شيء مفيد يُفعَل سوى الانهيار، أو عندما تكون الطبقة الأعلى هي التي تعرف كيفية التعافي.

إن دالةً في منتصف مكدّس الاستدعاء تبتلع كل خطأ وترجع بصمت تجعل الإخفاقات غير قابلة للتتبّع. فضّل ترك الاستثناءات تنتقل حتى تصل إلى شيفرة لديها خطة فعلية.