اذهب إلى المحتوى

كيفية التعامل مع الأخطاء البرمجية


أسامة دمراني

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

تجدر الإشارة إلى أن لغة VBScript هي أكثر لغة غريبة من اللغات الثلاثة التي ندرسها في معالجة الأخطاء، لأنها بُنيت على لغة BASIC، التي إحدى أوائل لغات البرمجة التي خرجت في عام 1963، وسنرى كيف ألقى تراثها بظلاله على VBScript فيما يتعلق بمعالجة الأخطاء، لكن هذا لا يؤثر على سياق شرحنا، بل سيعطينا الفرصة لشرح سبب سلوك VBScript بتعقب تاريخ معالجة الأخطاء، بدءًا من لغة BASIC، مرورًا بلغة Visual Basic حتى VBSCript، ثم ننظر بعدها إلى منظور أحدث، ونرى أمثلةً له في بايثون وجافاسكربت.

لقد كُتبَت البرامج في لغة BASIC مع أرقام للأسطر لتمييزها، حيث يُنقل التحكم بالقفز إلى سطر بعينه باستخدام تعليمة GOTO التي رأينا مثالًا لها في مقال مقدمة في البرمجة الشرطية، وقد كانت هذه صورة التحكم الوحيدة المتاحة وقتها، حيث كان الأسلوب الشائع لمعالجة الأخطاء حينئذ هو التصريح عن متغير errorcode الذي يخزن قيمةً عدديةً، وكلما حدث خطأ في البرنامج، سيُضبط المتغير errorcode ليعكس المشكلة. فإما أن يخبرنا أنه لم يستطع فتح الملف، أو أن النوع غير متطابق، أو حدث طفح للعوامل operator overflow، أو غير ذلك، وقد أدى هذا إلى شيفرة تشبه المثال التالي من برنامج وهمي:

1010 LET DATA = INPUT FILE
1020 CALL DATA_PROCESSING_FUNCTION
1030 IF NOT ERRORCODE = 0 GOTO 5000
1040 CALL ANOTHER_FUNCTION
1050 IF NOT ERRORCODE = 0 GOTO 5000
1060 REM CONTINUE PROCESSING LIKE THIS
...
5000 IF ERRORCODE = 1 GOTO 5100
5010 IF ERRORCODE = 2 GOTO 5200
5020 REM MORE IF STATEMENTS
...
5100 REM HANDLE ERROR CODE 1 HERE
...
5200 REM HANDLE ERROR CODE 2 HERE

يفحص نصف البرنامج الرئيسي وجود خطأ، لكن ظهرت مع الوقت آلية أفضل، حيث تولى مفسر اللغة عملية التقاط الأخطاء ومعالجتها؛ ولو جزئيًا، كما يلي:

1010 LET DATA = INPUTFILE
1020 ON ERROR GOTO 5000
1030 CALL DATA_PROCESSING_FUNCTION
1040 CALL ANOTHER_FUNCTION
...
5000 IF ERRORCODE = 1 GOTO 5100
5010 IF ERRORCODE = 2 GOTO 5200

سمح هذا بالإشارة إلى المكان الذي توجد فيه شيفرة الخطأ بواسطة سطر واحد، ورغم أننا لا زلنا بحاجة إلى الدوال التي اكتشفت الخطأ لضبط قيمة ERRORCODE، إلا أنها جعلت كتابة الشيفرة وقراءتها أسهل بكثير، لكن كيف يتأثر المبرمجون بهذا الأمر؟ توفر Viusal Basic إلى الآن هذا النوع من معالجة الأخطاء -على الرغم من استخدامنا حاليًا طريقةً أفضل من أرقام الأسطر-، وبما أن VBScript تنحدر من Visual Basic، فإنها توفر نسخةً مختصرةً للغاية من هذه الطريقة، وهي تخيّرنا بين معالجة الأخطاء محليًا أو تجاهلها تمامًا، ونستخدم الشيفرة التالية لتجاهل الأخطاء:

On Error Goto 0  ' 0 implies go nowhere
SomeFunction()
SomeOtherFunction()
....

أما لمعالجتها محليًا فنستخدم ما يلي:

On Error Resume Next
SomeFunction()
If Err.Number = 42 Then
   ' handle the error here
SomeOtherFunction()
...

يبدو هذا المنطق معكوسًا، لكنه يوضح العملية تاريخيًا كما أوضحنا أعلاه، فالسلوك الافتراضي للمفسر هو توليد رسالة إلى المستخدم، وإيقاف تنفيذ البرنامج إذا اكتشف خطأً ما، وهذا ما يحدث مع معالجة خطأ GoTo 0، فما هي إلا طريقة لإيقاف التحكم المحلي والسماح للمفسر بالعمل كالمعتاد.

تسمح لنا تعليمة Resume Next بالتظاهر وكأن الخطأ لم يحدث، أو أن التحقق من كائن الخطأ -الذي يسمى Err-، وسمة العدد -مثل تقنية errorcode الأولى-، كما أن للكائن Err أجزاء معلومات أخرى قد تفيدنا في التعامل مع الموقف بطريقة أفضل من مجرد إيقاف البرنامج، بحيث نستطيع معرفة مصدر الخطأ مثلًا، سواءً كان كائنًا أم دالةً أم غير ذلك، كما نستطيع الحصول على وصف نصي نستخدمه في تعبئة رسالة تخبر المستخدم بما يحدث، أو كتابة ملاحظة في ملف السجل.

كما يمكن تغيير نوع الخطأ باستخدام التابع Raise للكائن Err، إلى جانب استخدامنا له لتوليد أخطائنا من داخل دوالنا، لننظر في مثال حالة القسمة على الصفر؛ وهي حالة شائعة، لنرى معالجة الأخطاء في VBScript:

<script type="text/vbscript">
Dim x,y,Result
x = Cint(InputBox("Enter the number to be divided"))
y = CINt(InputBox("Enter the number to divide by"))
On Error Resume Next
Result = x/y
If Err.Number = 11 Then ' Divide by zero
   Result = Null
End If
On Error GoTo 0 ' turn error handling off again
If VarType(Result) = vbNull Then
   MsgBox "ERROR: Could not perform operation"
Else
   MsgBox CStr(x) & " divided by " & CStr(y) & " is " & CStr(Result)
End If
</script>

هذا الأسلوب غير مثالي، ورغم أن تقدير التراث البرمجي هنا جميل ولطيف، إلا أن لغات البرمجة الحديثة -بما فيها بايثون وجافاسكربت- لديها طرق أفضل لمعالجة الأخطاء، كما سنشرح في الجزئية الموالية من المقال، لكن قبل ذلك، ننصحك بالاطلاع على الفيديو الآتي لفهم الأخطاء البرمجية والتعرف على كيفية التعامل معها مهما اختلف نوعها ولغتها:

معالجة الأخطاء في بايثون

سنعرض فيما يلي آليات التعامل مع الأخطاء والاستثناءات التي تحصل أثناء تنفيذ شيفرة البرنامج وكيفية معالجتها في بايثون.

التعامل مع الاستثناءات

تتعامل لغات البرمجة الحديثة مع الاستثناءات exceptions وتعالجها بجعل الدوال ترفع الاستثناء raise أو تلقيه throw، ثم يفرض النظام قفزةً إلى خارج كتلة التعليمات البرمجية الحالية إلى أقرب كتلة معالجة استثناءات، ويوفر النظام معالجًا افتراضيًا يلتقط جميع الاستثناءات التي لم تعالَج في مكان آخر، كما يطبع رسالة خطأ ثم يخرج. انظر  إلى مقال  بداية رحلة تعلم البرمجة لمراجعة كيفية قراءة رسائل الخطأ في بايثون وتفسيرها، حيث تتمثل إحدى مزايا هذا النمط من معالجة الأخطاء في سهولة رؤية الوظيفة الأساسية للبرنامج، لأنها غير مختلطة بشيفرة معالجة الأخطاء، إذ نستطيع قراءة الكتلة الرئيسية دون الحاجة إلى النظر إلى شيفرة الخطأ مطلقًا. لننظر في كيفية عمل هذا النمط عمليًا:

استثناءات Try/Except

تُكتب كتلة معالجة الاستثناءات على شكل كتلة if ...then...else:

try:
   # منطق البرنامج هنا
except ExceptionType:
   # معالجة الاستثناءات للاستثناء المسمى هنا
except AnotherType:
   # معالجة الاستثناءات لاستثناءات أخرى هنا
else:
   # هنا نقوم بالترتيب إذا لم تُرفع استثناءات

تحاول بايثون أن تنفذ التعليمات بين try وأول تعليمة except، فإذا واجهت خطأً ما، فستوقف تنفيذ شيفرة block وتقفز إلى تعليمات except حتى تجد واحدةً تطابق نوع الخطأ أو الاستثناء، فإذا وجدت مطابقةً، فستنفذ الشيفرة التي في الكتلة التي بعد هذا الاستثناء مباشرةً؛ أما إذا لم توجد تعليمة except مطابِقة، فسيُنشر الخطأ إلى المستوى التالي للبرنامج، إلى أن توجد مطابقة، أو أن يكتشف مفسر المستوى الأعلى في بايثون هذا الخطأ ويعرض رسالة خطأ ويوقف تنفيذ البرنامج، وهو ما رأيناه في برامجنا إلى الآن.

أما إذا لم يوجد خطأ في كتلة try، فستنفَّذ كتلة else الأخيرة، رغم أن هذه الخاصية لا تُستخدم إلا نادرًا. لاحظ أن تعليمة except التي ليس فيها نوع خطأ محدد، إذ ستلتقط كل أنواع الأخطاء التي لم تعالَج بعد، وهذا سيئ إلا في حالة المستوى الأعلى في برنامجك حين تريد تجنب عرض رسائل بايثون التقنية إلى المستخدمين، حيث يمكن استخدام تعليمة استثناء عامة لالتقاط أي أخطاء غير ملتقطة، وعرض رسالة "إغلاق" مناسبة للمستخدم، ويجب الانتباه إلى تسجيل بيانات الخطأ في ملف السجل للتحليل في المستقبل.

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

لننظر الآن في مثال حقيقي لتوضيح الشرح:

value = input("Type a divisor: ")
try:
   value = int(value)
   print( "42 / %d = %d" % (value, 42/value) )
except ValueError: 
   print( "I can't convert the value to an integer" )
except ZeroDivisionError:
   print( "Your value should not be zero" )
except: 
   print( "Something unexpected happened" )
else: print( "Program completed successfully" )

إذا شغلنا هذا البرنامج وأدخلنا قيمةً ليست برقم مثل إدخال سلسلة نصية في المحث، فسنحصل على رسالة ValueError؛ أما إذا أدخلنا 0 فنحصل على رسالة ZeroDivisionError، وإذا ضغطنا Ctrl+C فسنرفع استثناء KeyboardInterrupt ونرى رسالةً تقول "Something unexpected happened"؛ أما إذا كتبنا عددًا صالحًا فسنحصل على النتيجة مع رسالة "Program Completed successfully".

استثناءات Try/Finally

ثمة نوع آخر من كتل الاستثناءات التي تسمح لنا بالترتيب بعد حدوث خطأ ما، وتسمى try...finally، كما تُستخدم لإغلاق الملفات واتصالات الشبكات أو قواعد البيانات، وتنفَّذ كتلة finally في النهاية بغض النظر عما يحدث في قسم try:

try:
   # المنطق المعتاد للبرنامج
finally:
   # try هنا نرتب بغض النظر عن نجاح كتلة
   # أو فشلها

تصبح الكتلة قويةً للغاية إذا جمعناها مع try/except:

print( "Program starting" )
try:
   data = open("data.dat")
   print( "data file opened" )
   value = int(data.readline().split()[2])
   print( "The calculated value is %s" % (value/(42-value)) )
except ZeroDivisionError: 
   print( "Value read was 42" )
finally:
   data.close()
   print( "data file closed" )

print( "Program completed" )

لاحظ أن ملف البيانات يجب أن يحتوي على سطر مع رقم في الحقل الثالث، كما يلي:

Foo bar 42

هنا يُغلق ملف البيانات دومًا بغض النظر عن رفع الاستثناء في كتلة try/except أو لا. لاحظ أن هذا السلوك مختلف عن شرط else لكتلة try/except، لأنه يُستدعى فقط عند عدم رفع استثناءات، كما يعني وضع الشيفرة خارج كتلة try/except أن الملف لم يغلَق إذا كان الاستثناء شيئًا غير ZeroDivisionError، ولا نضمن أن الملف مغلق إلا بإضافة كتلة finally. كذلك وضعنا تعليمة open()‎ داخل كتلة try/except، فإذا أردنا التقاط خطأ فتح ملف، فسنحتاج إلى إضافة كتلة except أخرى لـ IOError. جرب هذا بنفسك ثم افتح ملفًا غير موجود لترى ذلك عمليًا.

توليد الأخطاء

إذا أردنا توليد استثناءات ليلتقطها غيرنا في وحدة ما، فنستخدم الكلمة المفتاحية raise في بايثون:

numerator = 42
denominator = int( input("What value will I divide 42 by?") )
if denominator == 0:
   raise ZeroDivisionError

يرفع هذا استثناء ZeroDivisionError الذي يمكن التقاطه بواسطة كتلة try/except، أما بالنسبة لبقية البرنامج فسيبدو كما لو أن بايثون ولّدت هذا الخطأ داخليًا.

يمكن استخدام كلمة raise في توليد خطأ لمستوى أعلى في البرنامج من داخل كتلة الاستثناء، فقد نرغب في أخذ إجراء محلي، مثل تسجيل خطأ في ملف، ثم نسمح للمستوى الأعلى من البرنامج أن يقرر الإجراء النهائي، كما يلي:

def div127by(datum):
    try:
      return 127/(42-datum)
    except ZeroDivisionError:
      logfile = open("errorlog.txt","a")
      logfile.write("datum was 42\n")
      logfile.close()
      raise

try:
   div127by(42)
except ZeroDivisionError:
   print( "You can't divide by zero, try another value" )

لاحظ كيف تلتقط الدالة div127by()‎ الخطأ، وتسجل رسالةً في ملف الخطأ، ثم تمرر الاستثناء مرةً أخرى إلى كتلة try/except الخارجية لتتعامل معه باستدعاء raise دون كائن خطأ محدد. لنجمع هذين الجزأين معًا في برنامج واحد يوضح معالجة الأخطاء عمليًا:

def div127by(datum):
    try:
      return 127/(42-datum)
    except ZeroDivisionError:
      logfile = open("errorlog.txt","a")
      logfile.write("datum was 42\n")
      logfile.close()
      raise

try:
   divisor = int( input("What value will I divide by?") )
   if divisor == 0:
      raise ZeroDivisionError
   print( "The result is: ", div127by(divisor) )
except ZeroDivisionError:
   print( "You can't divide by zero, try another value" )

فإذا أدخل المستخدم 42 أو 0 فسينتِج ZeroDivisionError، مع أن 0 قيمة آمنة في هذه الحالة؛ أما غير ذلك فنطبع نتيجة القسمة ونسجل قيمة الدخل في الملف errorlog.txt.

الاستثناءات المعرفة من قبل المستخدم

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

لا يحتوي صنف الاستثناء عادةً على محتوىً خاص به، وإنما نعرف صنفًا فرعيًا من Exception، ونستخدمه مثل نوع من الملصقات الذكية التي يمكن التقاطها بواسطة تعليمات except. لننظر في هذا المثال القصير:

>>> class BrokenError(Exception): pass
...
>>> try:
...   raise BrokenError
... except BrokenError:
...   print( "We found a Broken Error" )
...

لاحظ أننا نستخدم اصطلاح تسمية نضيف فيه Error إلى نهاية اسم الصنف، وأننا نكتسب سلوك صنف Exception العام بإدراجه في أقواس بعد الاسم، كما سنتعلم الاكتساب أو الوراثة inheritance في البرمجة كائنية التوجه.

يجب ملاحظة نقطة أخيرة في رفع الأخطاء، وهي أننا كنا ننهي برامجنا باستيراد sys واستدعاء الدالة exit()‎، لكن يمكن استخدام أسلوب آخر يحقق نفس النتيجة، عن طريق رفع خطأ SystemExit()‎ كما يلي:

>>> raise SystemExit

وميزة هذا الأسلوب أننا لا نحتاج إلى import sys في البداية.

جافاسكربت

تعالج جافاسكربت الأخطاء بطريقة تشبه طريقة بايثون، باستخدام الكلمات المفتاحية try وcatch وthrow مقابل كلمات بايثون try وexcept وraise، وسننظر الآن في بعض الأمثلة، كما سنرى استخدام المبادئ نفسها التي كانت في بايثون، وقد أدخلت الإصدارات الأخيرة من جافاسكربت بنية finally، كما يمكن استخدام شرط finally في جافاسكربت مع كتلة try/catch في بنية واحدة. انظر توثيق جافاسكربت لمزيد من التفاصيل.

التقاط الأخطاء

تُلتقَط الأخطاء باستخدام كتلة try مع مجموعة من تعليمات catch تكاد تكون مطابقةً لما رأيناه في بايثون:

<script type="text/javascript">
try{
   var x = NonExistentFunction();
   document.write(x);
}
catch(err){
   document.write("We got an error in the code");
}
</script>

يكمن الاختلاف الأساسي في أننا نستخدم تعليمة catch واحدةً فقط لكل بنية try، وعلينا أن نفحص الخطأ الممرَّر داخل كتلة catch لنرى نوعه، وهذا فوضوي موازنةً بأسلوب except المتعدد في بايثون والمبني على نوع الاستثناء، وسنرى مثالًا بسيطًا لاختبار قيم الأخطاء في الشيفرة التالية.

رفع الأخطاء

يمكن رفع الأخطاء باستخدام الكلمة throw كما استخدمنا كلمة raise في بايثون، كما نستطيع إنشاء أنواع الخطأ الخاصة بنا في جافاسكربت كما فعلنا في بايثون، لكن الأسهل هو استخدام سلسلة نصية:

<script type="text/javascript">
try{
   throw("New Error");
}
catch(e){
   if (e == "New Error")
      document.write("We caught a new error");
   else
      document.write("An unexpected error found");
}
</script>

هذا كل ما سنشرحه حول معالجة الأخطاء، وسنرى أمثلةً عمليةً لها في المقالات التالية، كما سنرى بعض المفاهيم الأساسية التي تحدثنا عنها من قبل، مثل التسلسلات والحلقات التكرارية والفروع، مما يعني أن لديك جميع الأدوات اللازمة لإنشاء برامج قوية. يفضل الآن أن تأخذ وقتًا تحاول فيه إنشاء بعض البرامج بنفسك -ربما بضعة برامج فقط-، لتثبت تلك المفاهيم في رأسك قبل الانتقال إلى مجموعة المقالات التالية، وتستطيع البدء بالبرامج التالية:

  • لعبة بسيطة مثل OXO.
  • قاعدة بيانات بسيطة، ربما مبنية على دليل جهات الاتصال الخاص بنا، ولكن لتخزين مجموعة أقراصك أو مقاطع الفيديو.
  • أداة تسجيل يوميات تتيح لك إمكانية تخزين الأحداث أو التواريخ المهمة، وربما تخرج لك إشعارًا تذكيريًا.

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

خاتمة

نرجو في نهاية المقال أن تكون تعلمت ما يلي:

  • التحقق من شيفرات أخطاء VBScript باستخدام تعليمة if.
  • التقاط الاستثناءات بشرط except في بايثون أو catch في جافاسكربت.
  • توليد الاستثناءات باستخدام كلمة raise المفتاحية في بايثون أو throw في جافاسكربت.
  • أنه يمكن أن تكون أنواع الأخطاء أصنافًا في بايثون أو سلسلةً بسيطةً في جافاسكربت.

ترجمة -بتصرف- للفصل الرابع عشر: Handling Errors من كتاب Learning To Program لصاحبه Alan Gauld.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...