الاستثناءات — التعامل مع الأخطاء والانهيارات
التقاط خطأ محدد باستخدام try/except، لماذا يُعد except Exception: pass خطأ في كل الحالات تقريباً، كتلتا else و finally، إطلاق الاستثناءات باستخدام raise، واختيار الخطأ المناسب من شجرة الاستثناءات.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
في السؤال الأخير من الفصل السابق، قام البرنامج بمعالجة ثلاثة أسطر بنجاح ثم توقف تماماً عند السطر الرابع. ولتوضيح الأمر بصورة أبسط:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'تمت إضافة 10 بنجاح. لكن 30 لم تُضف إطلاقاً. والسطر print(total) لم يُنفذ قط.
سطر واحد تالف أوقف المهمة بأكملها. وحتى هذه اللحظة، كان أمامنا خياران فقط — إما أن تكون كل قطعة من البيانات سليمة تماماً، وإما أن ينهار البرنامج ويتوقف.
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
40ولكن هناك خيار ثالث أفضل: دع المشكلة تحدث، ثم تعامل معها بحكمة.
في نهاية هذا الدرس ستكون قادراً على
- التقاط خطأ محدد بدقة باستخدام
tryوexcept - شرح سبب كون كتابة
except:وحدها أوexcept Exception: passفكرة خاطئة وخطيرة في جميع الحالات تقريباً - الإمساك بكائن الخطأ عبر
as errوقراءة الرسالة التوضيحية الخاصة به - معرفة دور وتوقيت تشغيل كتلتي
elseوfinally - إطلاق أخطائك الخاصة باستخدام
raiseومعرفة متى يكون ذلك ضرورياً - التعرف على شجرة عائلات الاستثناءات والتقاط المستوى المناسب منها
المتطلبات السابقة: قراءة الملفات وكتابتها.
الخطأ هو كائن بحد ذاته
كتابة as تضع كائن الخطأ بين يديك:
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'النص الأحمر الذي كنت تراه يملأ الطرفية طوال هذه الفترة هو في الواقع هذه الكائنات، وطباعة الكائن تعرض رسالة التوضيح المرفقة به.
وهذا يقودنا إلى فائدة عملية بالغة الأهمية: يحمل كائن الخطأ أدق وأوضح تفسير لما حدث. طباعة err تكاد تكون دائماً أكثر فائدة من كتابة عبارة عامة من عندك مثل "حدث خطأ ما".
التقط الخطأ الصحيح فقط
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'تلتقط كتلة except فقط النوع الذي حددته بالاسم. هنا حددنا TypeError، بينما الخطأ الذي وقع فعلياً كان ValueError — لذا كأن كتلة except لم تكن موجودة على الإطلاق، وتوقف البرنامج.
قد يبدو ذلك عائقاً، لكنه في الواقع الميزة الأكثر فائدة! سطر except يعلن صراحة عن الخطأ الذي كنت مستعداً لمواجهته — وأي خطأ آخر لم تكن مستعداً له سيوقف البرنامج، وهو التصرف السليم تماماً.
ولهذا السبب تحديداً، يُحظر عليك كتابة كود كهذا:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40الإجابة صحيحة رياضياً، ومع ذلك فالكود سيئ للغاية ومضلل. عبارة except Exception: pass تبتلع كل شيء بصمت — الأعطال التي فكرت فيها وتلك التي لم تخطر ببالك. فإذا تحولت القائمة numbers غداً إلى قاموس عن طريق الخطأ، أو إذا أخطأت في كتابة اسم متغير بالداخل، فسيستمر هذا البرنامج في العمل ويعطيك بهدوء إجابة غير صحيحة دون أن تشعر.
القاعدة: لا تلتقط سوى الخطأ الذي تملك خطة واضحة للتعامل معه. وبمجرد التقاطه، تصرف حيال ذلك — على الأقل، اعرض ما حدث.
أكثر من نوع من الأخطاء
def get(data, key):
try:
return data[key]
except KeyError:
return "no such key"
except IndexError:
return "no such position"
print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))15
no such key
no such positionيمكن كتابة عدة كتل except متتالية، وأول كتلة تطابق نوع الخطأ هي التي تُنفذ — بينما يتم تجاهل البقية تماماً.
وعندما يكون التعامل المطلوب هو نفسه لأكثر من خطأ، يمكن دمجهما معاً بين قوسين:
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'الخاصية type(err).__name__ تعطي اسم نوع الخطأ — وهي مفيدة جداً عند كتابة سجلات التتبع (Logs).
الأخطاء تأتي في عائلات وراثية
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
Trueتترتب الأخطاء في بايثون على شكل شجرة عائلية وراثية، والتقاط الصنف الأب يلتقط جميع الأصناف الأبناء تلقائياً:
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: goneعلاقات وراثية شائعة يجدر بك معرفتها: FileNotFoundError و PermissionError كلاهما ينحدر من OSError؛ و KeyError و IndexError كلاهما ينحدر من LookupError؛ وكل الاستثناءات تقريباً تنحدر من الصنف الأساسي Exception.
ومن هنا تأتي القاعدة الذهبية: التقط أضيق نوع خطأ ممكن يتناسب مع حالتك. فكتابة except Exception هي الأسهل في الكتابة والأقل نفعاً على الإطلاق.
كتلتا else و finally
def read(text):
try:
value = int(text)
except ValueError:
print("bad:", text)
return None
else:
print("good:", value)
return value
finally:
print("done with", text)
print(read("10"))
print(read("ten"))good: 10
done with 10
10
bad: ten
done with ten
Noneكتلة else تُنفذ فقط عندما تكتمل كتلة try بنجاح دون أي خطأ. فائدتها أنها تبقي كتلة try صغيرة ومحددة — فتحتوي فقط على السطر الذي قد يتعطل — بينما العمل الذي يتبع النجاح يوضع في else. فإذا وضعت كوداً زائداً داخل try، فستلتقط أخطاء جانبية لم تكن تقصد معالجتها أصلاً.
كتلة finally تُنفذ دائماً وبلا استثناء، مهما حدث. حتى وإن انتهت الدالة بعبارة return:
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryعبارة return حددت القيمة المعادة بالفعل، لكن finally تُنفذ قبل أن تغادر الدالة فعلياً. وهي المكان المخصص لعمليات التنظيف وتحرير الموارد — مع الإشارة إلى أن استخدام with في التعامل مع الملفات قد تولى هذا العبء بالكامل.
إطلاق الأخطاء بنفسك (Raising Exceptions)
إطلاق الخطأ يضاهي في الأهمية التقاطه.
def line_total(price, quantity):
if quantity < 0:
raise ValueError(f"quantity cannot be negative: {quantity}")
return price * quantity
print(line_total(15.0, 3))
print(line_total(15.0, -1))45.0
ValueError: quantity cannot be negative: -1ماذا كان البديل عن استخدام raise؟ إرجاع None، أو إرجاع 0 — وحينها سيتسرب الخطأ ويتسلل بصمت. سيندمج في حسابات إجمالية أخرى، ليطفو على السطح لاحقاً كرقم غريب غير مفهوم ودون أي أثر يوضح مصدره الأصلي.
الخطأ الصريح يوقف المشكلة في مهدها لحظة ولادتها، ويوضح السبب بدقة في رسالته.
وعند كتابة تلك الرسالة، ضمّن القيمة المسببة للخطأ بداخلها. الفرق بين "invalid quantity" و "quantity cannot be negative: -1" شاسع — فالرسالة الثانية وحدها تمنحك نقطة بداية للحل.
يمكنك أيضاً التقاط خطأ وإعادة إطلاقه برسالة أوضح وأشمل:
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
ValueError: not a number: 'ten'الصيغة {text!r} تدرج القيمة عبر دالة repr، مما يظهر علامات الاقتباس — والفرق بين not a number: 'ten' و not a number: ten بالغ الأهمية إذا كانت القيمة نصاً فارغاً تماماً أو مسافة بيضاء خفية.
الاستفسار أولاً، أم الاعتذار لاحقاً؟
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedكلا الأسلوبين صحيح. والأسلوب الثاني أكثر شيوعاً في أوساط بايثون، لسبب وجيه: الأسلوب الأول يفحص مرتين — مرة عبر in، ومرة أخرى عند الاستخراج عبر [...] — وقد يتعطل إذا تغير شيء بين العمليتين.
أما بالنسبة للمسارات المعتادة المتكررة بكثرة، فإن جملة if تكون أكثر وضوحاً في القراءة. فالاستثناءات صُممت للحالات الاستثنائية؛ واسمها يدل على ذلك.
مثال متكامل
ملف orders.txt، ويحتوي عمداً على سطرين تالفين:
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0ملف main.py:
"""Read an order file, and keep going when a line is wrong."""
from pathlib import Path
TAX_RATE = 0.15
def parse_line(raw, number):
"""Returns one order line, or raises ValueError saying which line was wrong."""
parts = raw.split(",")
if len(parts) != 3:
raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")
name, price, quantity = parts
try:
return name, float(price), int(quantity)
except ValueError as err:
raise ValueError(f"line {number}: {err}")
def read_order(path):
"""Returns the good lines and the complaints, never raising for bad data."""
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return [], [f"no such file: {path}"]
lines = []
problems = []
for number, raw in enumerate(text.splitlines(), start=1):
if not raw.strip():
continue
try:
lines.append(parse_line(raw, number))
except ValueError as err:
problems.append(str(err))
return lines, problems
def main():
lines, problems = read_order(Path("orders.txt"))
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
print(f"{name:<6} {amount:>9.2f}")
print(f"{'total':<6} {total:>9.2f}")
print()
print(f"{len(lines)} lines used, {len(problems)} skipped")
for problem in problems:
print(" -", problem)
if __name__ == "__main__":
main()pen 51.75
ink 276.00
total 327.75
2 lines used, 2 skipped
- line 2: could not convert string to float: 'eight'
- line 5: expected 3 fields, got 2أربعة أمور جديرة بالدراسة:
parse_line تطلق الخطأ و read_order تلتقطه. الدالة الدنيا لا تملك أدنى فكرة عما يجب أن يفعله البرنامج ككل — كل ما تعرفه هو أن هذا السطر تحديداً غير قابل للاستخدام. أما اتخاذ القرار بشأن ما يجب فعله فيعود للدالة الأعلى منها. تُطلق الأخطاء في المستويات الدنيا وتُلتقط في المستويات العليا، وهذا الفصل في المسؤوليات هو جوهر نظام الاستثناءات.
رقم السطر جزء من رسالة الخطأ. الرسالة could not convert string to float: 'eight' لا تخبرك أين تبحث؛ أما line 2: ... فتخبرك بدقة. أي معلومة تعرفها لحظة الخطأ ولن تتوفر لك لاحقاً يجب أن تكون جزءاً من الرسالة.
read_order لا تنهار أبداً. إنها ترجع عنصرين دائماً — ما كان صالحاً للاستخدام، وقائمة المشكلات. وإذا كان الملف غير موجود أصلاً، فإنها ترجع إجابة بنفس البنية، فلا تضطر دالة main للتعامل مع مسارين منفصلين.
كتلة try الداخلية لا تتعدى سطراً واحداً. السطران float(price) و int(quantity) — فقط الجزء المعرض للفشل وُضع بالداخل. أما parts = raw.split(",") فخارجها، لأنه لا يُفترض به الفشل، وإن فشل فمن المهم جداً أن نعلم ذلك فوراً.
حالات الخطأ الشائعة
لم يتم التقاط الخطأ رغم وجود كتلة except نوع الخطأ غير مطابق. الكلمة الأولى في رسالة الخطأ هي اسم نوعه — حدد ذلك الاسم بالضبط، أو التقط الصنف الأب الذي ينحدر منه.
توجد كتلة except ومع ذلك لا يزال البرنامج يتوقف السطر الذي تعطل ليس واقعاً داخل كتلة try. تأكد من المسافات البادئة (Indentation).
SyntaxError: expected 'except' or 'finally' block تمت كتابة try دون أن تتبعها كتلة except أو finally. لا يمكن لكتلة try أن تقف وحدها.
يبدو البرنامج وكأنه "يعمل" لكن النتائج غير صحيحة توجد عبارة except Exception: pass في مكان ما. ابحث عنها — فهناك تكمن المشكلة الحقيقية.
أطلقت خطأ لكن الرسالة لا تظهر كتبت raise ValueError بدلاً من raise ValueError("..."). أضف القوسين والنص التوضيحي.
عبارة return داخل finally تبتلع الخطأ وتخفيه هذا صحيح، ولهذا السبب تحديداً يجب ألا تضع return داخل finally. فكتلة finally مخصصة للتنظيف فقط.
Step 4 of 6 — Predict
Check your understanding
There is an except. What happens to the program?
try:
int("twenty")
except TypeError:
print("caught")- AThe program stops with a `ValueError` — the `except` may as well not be there
- B`caught` prints
- CNothing prints and the program ends quietly
- DIt stops with a `TypeError`
The whole loop is inside the try. What is printed?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 10
- Bgave up 40
- Cgave up 0
- D40
Both the try and the except have a return. What is printed?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 checked 7 0 checked seven
- Cchecked 7 7 0
- D7 0
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
عد إلى سكربت summary.py من الفصل السابق، والذي يقرأ ملف people.txt.
قم بتعديله الآن بحيث لا يتعطل مطلقاً مهما كانت البيانات، وينتهي بعرض الأسطر التي تم تخطيها مع توضيح سبب تخطي كل منها. اكتب دالة parse_line(raw, number) تطلق ValueError يتضمن رقم السطر عند مواجهة سطر تالف.
ثم قم بإفساد الملف عمداً — احذف الفاصلة من أحد الأسطر، وضع أحرفاً بدلاً من الأرقام في سطر آخر، واترك سطراً فارغاً تماماً — وتأكد من أن البرنامج يعمل حتى نهايته مع تقديم تقرير بجميع المشكلات.
ثم أجرِ هذه التجارب الأربع العملية:
- استبدل
except ValueErrorبـexcept Exception: pass. كيف ستكون المخرجات، ولماذا يُعتبر هذا التصرف خطيراً جداً؟ - احذف رقم السطر من الرسالة في دالة
parse_line. كم من الوقت ستستغرق الآن للعثور على السطر المعيب؟ - وسّع نطاق كتلة
try— ضع جسم حلقة التكرار بأكمله داخلها. ماذا سيحدث الآن إذا كان سطر واحد فقط تالفاً؟ - أضف كتلة
finallyتطبع رسالة بعد كل سطر، وتحقق من أنها تعمل مع الأسطر السليمة والمعيبة على حد سواء.
التجربة الثالثة ترسخ أهم عادة في هذا الفصل: كلما كانت كتلة try أصغر، كان التقاط الأخطاء أكثر نزاهة ودقة.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz