# الاستثناءات في Python: try وexcept وelse وfinally وraise والاستثناءات المخصصة

URL: https://skillaria.top/ar/guides/python/exceptions-python

الاستثناء في Python كائن يمثل خطأً ويوقف التنفيذ العادي حتى يلتقطه كتلة try/except. تُنفَّذ else إذا لم يحدث خطأ، وتُنفَّذ finally دائماً، ويرفع الخطأ يدوياً بالتعليمة raise. ويُقرأ الـ traceback من الأسفل، وتُلتقط الأخطاء المحددة لا كل شيء، لأن التقاط كل شيء يخفي المشكلات الحقيقية.

الاستثناء في Python كائن يدل على خطأ ويوقف المجرى الطبيعي للبرنامج. إذا لم يلتقطه أحد يطبع Python الـ traceback وينتهي التنفيذ. يُلتقط الاستثناء بالتركيب `try/except`، ويُرفع يدوياً بالتعليمة `raise`. وفيما يلي شرح لطريقة عمل ذلك، وأكثر الأخطاء شيوعاً، وكيف تعالج الأخطاء دون أن تخفيها.

## ما هو الاستثناء في Python

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

```
>>> 10 / 0
Traceback (most recent call last):
  File "", line 1, in
ZeroDivisionError: division by zero
```

وإجابة على سؤال متكرر: القسمة على صفر في Python تُنتج الاستثناء `ZeroDivisionError`. وهذا ينطبق على `/` و`//` و`%`.

## كيف تقرأ الـ traceback

الـ traceback تقرير عن الطريق الذي وصل منه البرنامج إلى الخطأ. ويُقرأ من الأسفل إلى الأعلى. السطر الأخير هو الأهم: نوع الاستثناء ونصّ الرسالة. وفوقه سطر الكود الذي وقع فيه الخطأ، وفوقه من استدعاه، وهكذا حتى نقطة الدخول.

```
Traceback (most recent call last):
  File "tracker.py", line 9, in
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'مئة'
```

منذ Python 3.11 ترسم الرموز `~~~^^^` تحت سطر الكود (PEP 657)، فتُظهر التعبير الدقيق الذي تعطل داخل السطر. وهذا مفيد جداً عندما تتسلسل عدة استدعاءات في سطر واحد.

### تلميحات «Did you mean»

منذ Python 3.10 تقترح رسالة الخطأ، عند وجود خطأ مطبعي في الاسم، أقرب تسمية صحيحة. وفي Python 3.15 صارت التلميحات أذكى: يعرف المفسّر العادات القادمة من لغات أخرى. فاستدعاء `list.push` يقترح `append`، واستدعاء `"قهوة".toUpperCase()` يقترح الطريقة `upper`. وتمتد التلميحات إلى الحقول المتداخلة: إذا كان القاموس موجوداً في الخاصية `totals` للكائن، واستدعيت `items()` على الكائن نفسه، فستقترح الرسالة `.totals.items`. كما تقترح الدالة `delattr()` الأسماء القريبة أيضاً.

## أنواع الاستثناءات في Python

الاستثناءات المدمجة عشرات، لكنك ستراها أكثر من غيرها في البداية:

| الاستثناء | متى يحدث | مثال |
| --- | --- | --- |

| `NameError` | اسم غير معرّف، غالباً خطأ مطبعي | `print(totl)` |

| `TypeError` | العملية لا تناسب هذا النوع | `"5" + 3` |

| `ValueError` | النوع صحيح لكن القيمة لا | `int("مئة")` |

| `KeyError` | المفتاح غير موجود في القاموس | `d["missing"]` |

| `IndexError` | الفهرس خارج حدود القائمة | `[1, 2][5]` |

| `AttributeError` | الكائن لا يملك هذه الطريقة أو الخاصية | `[].push(1)` |

| `ZeroDivisionError` | القسمة على صفر | `1 / 0` |

| `FileNotFoundError` | الملف غير موجود | `open("nope.txt")` |

يستحق `SyntaxError` (وحالته الخاصة `IndentationError`) ملاحظة خاصة: يظهر أثناء قراءة الملف قبل تشغيل الكود، لذلك لا يمكن التقاطه بـ `try` في الملف نفسه.

### التسلسل الهرمي للاستثناءات

الاستثناءات أصناف مرتبة في شجرة. في القمة `BaseException`، ومنه ينحدر `Exception` (معظم الأخطاء العادية) وبعض الاستثناءات النظامية: `KeyboardInterrupt` و`SystemExit` و`GeneratorExit`.

```
BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError
```

التسلسل مهم عند الالتقاط: `except LookupError` تلتقط `KeyError` و`IndexError` معاً. و`KeyboardInterrupt` ليس خطأً في الكود، بل إشارة من Python بأن المستخدم ضغط Ctrl+C. ولا يرث من `Exception` عمداً، حتى لا تمنع `except Exception` إيقاف البرنامج.

## معالجة الاستثناءات: try وexcept وelse وfinally

يوضع الكود الذي قد يفشل داخل كتلة `try`. وفي كتلة `except` تحدد نوع الخطأ وما ستفعله به. وكتلة `except` هي التي تُستخدم في Python لمعالجة الاستثناءات.

```
def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"أتجاوز السطر: {e}")
        return None
    else:
        return value          # تُنفَّذ إذا لم يحدث خطأ
    finally:
        print("انتهى التحليل")   # تُنفَّذ دائماً

print(parse_amount("250"))
```

ترتيب الطباعة هنا: أولاً «انتهى التحليل»، ثم `250.0`. إذ يحفظ `return` الموجود في كتلة `else` القيمة، ثم ينفّذ Python كتلة `finally` بالضرورة، وبعدها فقط يعيد النتيجة. لذلك كتلة `finally` موثوقة للتنظيف: إغلاق الملف، وتحرير الاتصال.

إذا كتبت `return` داخل `finally` فإنه يلغي الذي قبله، وقد «يبتلع» الاستثناء. ومنذ Python 3.14 يصدر لهذا الكود تحذير `SyntaxWarning` (PEP 765)، فلا تكتب `return` داخل `finally`.

### عدة استثناءات معاً

يمكن كتابة عدة كتل `except` لأنواع مختلفة، أو ذكر الأنواع في صف واحد:

```
try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("مبلغ غير صالح:", e)
    amount = 0
```

تُفحص الكتل من الأعلى إلى الأسفل، وتُنفَّذ أول كتلة مطابقة، لذلك تُوضع الأنواع الأدق فوق الأعم. ومنذ Python 3.14 (PEP 758) يمكن ذكر عدة أنواع دون أقواس، مثل `except ValueError, TypeError:`، لكن إذا وُجدت `as` فالأقواس واجبة.

### كيف تعرض نص الخطأ وتعرف نوعه

يتوفر كائن الاستثناء عبر `as e`. فـ `str(e)` تعطي الرسالة، و`type(e).__name__` تعطي اسم النوع:

```
try:
    int("مئة")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: 'مئة'
```

## الوحدة traceback: النص الكامل للخطأ

أحياناً تريد أن تلتقط الخطأ لكن تحفظ الـ traceback كاملاً، مثلاً في ملف سجل أو تقرير. لذلك توجد الوحدة `traceback`:

```
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # يطبع الـ traceback في stderr
    text = traceback.format_exc()     # النص نفسه كسلسلة
```

إذا كنت تكتب السجلات بالوحدة `logging`، فالأسهل استدعاء `logging.exception("تعذر الحساب")` داخل `except`، فيُضاف الـ traceback إلى الرسالة تلقائياً.

## raise في Python: رفع استثناء

التعليمة `raise` ترفع استثناءً يدوياً. وبهذا تعلن الدالة أنها لا تستطيع العمل مع هذه البيانات:

```
def add_expense(amount):
    if amount < 0:
        raise ValueError(f"المبلغ لا يمكن أن يكون سالباً: {amount}")
    ...

add_expense(-5)
# ValueError: المبلغ لا يمكن أن يكون سالباً: -5
```

الأمر `raise` في Python تعليمة وليست دالة، لذلك لا تُكتب بعدها أقواس: الوسائط تُمرَّر إلى منشئ الاستثناء داخل القوسين. اختر النوع حسب المعنى: القيمة الخاطئة تكون `ValueError`، والنوع الخاطئ يكون `TypeError`.

### raise بلا وسائط وraise from

داخل `except` تعيد `raise` المجردة رفع الاستثناء الحالي كما هو. وهذا مفيد عندما تريد تسجيل الخطأ في السجل، وتترك المعالجة لمن استدعى الدالة.

وإذا استبدلت استثناءً باستثناء آخر فاستخدم `raise ... from e`: يبقى الخطأ الأصلي محفوظاً في السلسلة، ويظهر الاثنان في الـ traceback:

```
def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"الإعدادات غير موجودة: {path}") from e
```

### add_note: إضافة سياق للخطأ

منذ Python 3.11 (PEP 678) يمكن إضافة ملاحظة إلى الاستثناء بالطريقة `add_note()` ثم رفعه من جديد. ولا يتغير نوع الخطأ، لكن الملاحظة تُطبع تحت الرسالة:

```
def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"الملف expenses.csv، السطر {line_no}")
        raise

parse("مئة", 17)
# ValueError: could not convert string to float: 'مئة'
# الملف expenses.csv، السطر 17
```

## كيف تكتب استثناءً خاصاً بك في Python

الاستثناءات الخاصة مفيدة لأخطاء مجال العمل: تجاوز الميزانية، أو طلب مدفوع من قبل، أو مستخدم محظور. يكفي صنف يرث من `Exception`:

```
class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: الحد {limit}، المصروف {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("طعام", 10000, 12500)
except BudgetExceeded as e:
    print("تنبيه:", e)   # تنبيه: طعام: الحد 10000، المصروف 12500
```

الآن يلتقط الكود المستدعي تجاوز الميزانية تحديداً، ولا يخلط بينه وبين الأخطاء المطبعية. وفي المشروع الكبير يُفضَّل إنشاء صنف أساسي مشترك (`class TrackerError(Exception)`) ترث منه كل استثناءاتك، فتُلتقط بـ `except TrackerError` واحدة. وارث من `Exception` لا من `BaseException`.

## مجموعات الاستثناءات و except*

أحياناً تحدث عدة أخطاء معاً، كأن تفحص ملفاً كاملاً، أو في المهام المتوازية مع `asyncio.TaskGroup`. منذ Python 3.11 يمكن جمعها في `ExceptionGroup`، ومعالجتها حسب الأنواع بـ `except*`:

```
def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("أخطاء في الملف", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"سطور معطوبة: {len(eg.exceptions)}")   # سطور معطوبة: 2
```

## try except لكل الأخطاء: لماذا لا يجوز ذلك

أسوأ عادة هي التقاط كل شيء: `except: pass`. يتوقف البرنامج عن الانهيار، لكنه في الحقيقة يتوقف عن الإبلاغ عن المشكلات. فالـ `except` المجردة تلتقط حتى `KeyboardInterrupt` و`SystemExit`، فلا يمكن إيقاف البرنامج بـ Ctrl+C. و`except Exception: pass` أفضل قليلاً، لكنها تبتلع بصمت الأخطاء المطبعية في الأسماء وأخطاء المنطق.

كيف تعالج الاستثناءات بصورة صحيحة في Python:

- التقط النوع الذي تعرف كيف تعالجه فقط، مثل `except ValueError`، لا كل شيء؛
- اجعل كتلة `try` قصيرة، وتضم السطور التي قد تفشل بذلك النوع تحديداً؛
- إذا التقطت بشكل واسع (`except Exception`) في المستوى الأعلى للبرنامج، فسجّل الـ traceback دائماً؛
- لا تستخدم الاستثناءات للتفريع العادي حيث يكفي فحص: مثلاً `dict.get()` بدلاً من `try/except KeyError` في الحالات البسيطة؛
- للموارد استخدم `with`، فهو يغلق الملف بنفسه حتى عند الخطأ.

## الخلاصة

الاستثناءات في Python آلية طبيعية للإبلاغ عن الأخطاء، وليست سبباً لإخفائها. اقرأ الـ traceback من الأسفل، والتقط الأنواع المحددة في `try/except`، واستخدم `else` للكود الذي يعمل «إذا نجح كل شيء»، و`finally` للتنظيف. وتسمح `raise` بالإبلاغ عن الخطأ بنفسك، وتحفظ `raise from` و`add_note` السياق، وترث استثناءاتك الخاصة من `Exception`. وقد شُرح هذا الموضوع عملياً في الدورة، حيث يتوقف متتبّع المصروفات عن الانهيار بسبب سطر CSV واحد معطوب، ويعبّر عن تجاوز الميزانية باستثناء خاص به.

## FAQ

**ما الفرق بين الخطأ والاستثناء في Python؟**

في الكلام اليومي هما مترادفان. تقنياً، يظهر SyntaxError أثناء قراءة الكود، أما بقية الأخطاء فتنشأ أثناء التنفيذ كأشياء استثنائية يمكن التقاطها.

**هل يمكن متابعة التنفيذ بعد الاستثناء من المكان نفسه؟**

لا. بعد الالتقاط يستمر التنفيذ بعد كتلة try/except، لا من السطر الذي وقع فيه الخطأ. وإذا أردت معالجة كل سطر من ملف فضع try داخل الحلقة.

**ماذا يحدث إذا وقع استثناء داخل except؟**

يصعد الاستثناء الجديد إلى الأعلى، ويُحفظ الاستثناء الأصلي في الخاصية __context__ الخاصة به. وتظهر في الـ traceback العبارة During handling of the above exception, another exception occurred.

**كيف أتحقق في الاختبارات من أن الدالة ترفع استثناءً؟**

في pytest تُستخدم العبارة with pytest.raises(ValueError):، وفي unittest تُستخدم self.assertRaises(ValueError). ينجح الاختبار فقط إذا وقع استثناء من النوع المطلوب داخل الكتلة.

**ما هو assert وما الفرق بينه وبين raise؟**

العبارة assert شرط, "رسالة" ترفع AssertionError إذا كان الشرط خاطئاً. وهي أداة للتحقق أثناء التطوير، وعند التشغيل بالعلم -O تُعطَّل، لذلك تحقق من مدخلات المستخدم بـ if و raise.


---
Source: Skillaria.Top (https://skillaria.top)
