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

تنقيح أخطاء Debugging شيفرتك البرمجية باستخدام لغة بايثون


Ola Abbas

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

يمكن القول أن كتابة الشيفرة البرمجية تمثّل 90% من عملية البرمجة، ولكن يمثّل تنقيح أخطاء هذه الشيفرة البرمجية نسبة 90% أخرى من العمل، حيث سينفّذ حاسوبك ما تطلب منه فقط، إذ لن يقرأ أفكارك أو يطبّق ما تنوي فعله. يتسبّب المبرمجون وحتى المحترفون منهم بالأخطاء طوال الوقت، لذلك لا تشعر بالإحباط إذا واجه برنامجك مشكلةً ما.

يوجد عددٌ من الأدوات والتقنيات لتحديد ما تفعله شيفرتك البرمجية بالضبط ومكان حدوث الخطأ لحسن الحظ، حيث سنوضّح في هذا المقال أولًا التسجيل Logging والتأكيدات Assertions، وهما ميزتان مساعدتان في اكتشاف الأخطاء في وقت مبكر، إذ يصبح إصلاح الأخطاء أسهل كلما اكتشفتَها مبكرًا. سنوضّح بعد ذلك كيفية استخدام منقّح الأخطاء Debugger الذي يمثّل ميزةً من ميزات المحرّر Mu، حيث ينفّذ منقّح الأخطاء البرنامج من خلال تنفيذ تعليمةٍ واحدة في كل مرة، مما يمنحك فرصة لفحص القيم في المتغيرات أثناء تشغيل شيفرتك البرمجية وتعقّب كيفية تغير هذه القيم عبر برنامجك بأكمله. يُعَد ذلك أبطأ بكثير من تشغيل البرنامج بأقصى سرعة، ولكنه مفيد لرؤية القيم الفعلية في البرنامج أثناء تشغيله بدلًا من استنتاج ما ستكون عليه القيم من الشيفرة المصدرية.

رفع الاستثناءات Raising Exceptions

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

تُرفَع الاستثناءات باستخدام التعليمة raise التي تتكون ممّا يلي:

  • الكلمة المفتاحية raise.
  • استدعاء الدالة Exception()‎.
  • سلسلة نصية تحتوي على رسالة خطأ مفيدة نمرَّرها إلى الدالة Exception()‎.

لندخِل مثلًا ما يلي في الصدفة التفاعلية Interactive Shell:

>>> raise Exception('This is the error message.')
Traceback (most recent call last):
  File "<pyshell#191>", line 1, in <module>
    raise Exception('This is the error message.')
Exception: This is the error message.

إن لم تُوجَد تعليمات try و except المغلِّفة للتعليمة except التي ترفع الاستثناء، فسيتعطل البرنامج ويعرض رسالة خطأ الاستثناء ببساطة.

تعرِف الشيفرة البرمجية التي تستدعي الدالة -وليس الدالة نفسها- كيفية التعامل مع الاستثناء، وهذا يعني أنك سترى التعليمة raise ضمن الدالة وتعليمتي try و except في الشيفرة البرمجية التي تستدعي هذه الدالة. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد، وأدخِل مثلًا الشيفرة البرمجية التالية واحفظ البرنامج بالاسم boxPrint.py:

def boxPrint(symbol, width, height):
    if len(symbol) != 1:
       raise Exception('Symbol must be a single character string.')
    if width <= 2:
       raise Exception('Width must be greater than 2.')
    if height <= 2:
       raise Exception('Height must be greater than 2.')

    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        boxPrint(sym, w, h)
   except Exception as err:
       print('An exception happened: ' + str(err))

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

لنفترض أننا نريد أن يكون هذا المحرف مفردًا، وأن يكون العرض والارتفاع أكبر من 2، فسنضيف تعليمات if لرفع الاستثناءات عند عدم استيفاء هذه المتطلبات. ستتعامل لاحقًا تعليمات try/except مع الوسطاء غير الصالحة عندما نستدعي الدالة ‎boxPrint()‎ مع وسطاء مختلفة.

يستخدم هذا البرنامج الصيغة except Exception as err للتعليمة except ➍. إذا أُعيد الكائن Exception من الدالة boxPrint()‎ ➊ ➋ ➌، فستخزِّنه التعليمة except في متغير بالاسم err، ثم يمكننا تحويل الكائن Exception إلى سلسلة نصية من خلال تمريره إلى الدالة ‎str()‎ لإعطاء رسالة خطأ مألوفة للمستخدم ➎، وسيبدو الخرج كما يلي عند تشغيل البرنامج boxPrint.py:

****
*  *
*  *
****
OOOOOOOOOOOOOOOOOOOO
O                  O
O                  O
O                  O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.

يمكنك التعامل مع الأخطاء بأمان أكبر باستخدام تعليمات try و except بدلًا من ترك البرنامج يتعطل بأكمله.

الحصول على التعقب العكسي Traceback كسلسلة نصية

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

افتح تبويبًا جديدًا في محرّر Mu لإنشاء ملف جديد، وأدخِل البرنامج التالي واحفظه بالاسم errorExample.py:

def spam():
    beef()

def beef():
    raise Exception('This is the error message.')

spam()

سيبدو الخرج كما يلي عند تشغيل البرنامج errorExample.py:

Traceback (most recent call last):
  File "errorExample.py", line 7, in <module>
    spam()
  File "errorExample.py", line 2, in spam
    beef()
  File "errorExample.py", line 5, in beef
    raise Exception('This is the error message.')
Exception: This is the error message.

يمكنك أن ترى من التعقّب العكسي السابق أن الخطأ حدث في السطر رقم 5 في الدالة beef()‎، وأتى هذا الاستدعاء للدالة beef()‎ من السطر رقم 2 في الدالة spam()‎ التي اُستدعيت بدورها في السطر رقم 7. يمكن أن يساعدك مكدس الاستدعاءات في تحديد الاستدعاء الذي أدى إلى الخطأ في البرامج التي يمكن فيها استدعاء الدوال من أماكن متعددة.

تعرض لغة بايثون التعقّب العكسي عند عدم التعامل مع الاستثناء المرفوع، ولكن يمكنك أيضًا الحصول على التعقب العكسي بوصفه سلسلة نصية من خلال استدعاء الدالة traceback.format_exc()‎، حيث تكون هذه الدالة مفيدة إذا أردتَ الحصول على المعلومات من التعقب العكسي الخاص بالاستثناء ولكنك تريد أيضًا التعليمة except للتعامل مع الاستثناء بأمان. يجب استيراد الوحدة traceback الخاصة بلغة بايثون قبل استدعاء هذه الدالة.

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

>>> import traceback
>>> try:
...          raise Exception('This is the error message.')
except:
...          errorFile = open('errorInfo.txt', 'w')
...          errorFile.write(traceback.format_exc())
...          errorFile.close()
...          print('The traceback info was written to errorInfo.txt.')


111
The traceback info was written to errorInfo.txt.

تُعَد القيمة 111 هي القيمة التي يعيدها التابع write()‎، حيث كُتِبت المحارف 111 في الملف، وكُتِب نص التعقّب العكسي في الملف errorInfo.txt:

Traceback (most recent call last):
  File "<pyshell#28>", line 2, in <module>
Exception: This is the error message.

سنتعلّم لاحقًا كيفية استخدام الوحدة logging التي تُعَد أكثر فعالية من مجرد كتابة معلومات الخطأ في ملفات نصية.

التأكيدات Assertions

يُعَد التأكيد Assertion فحص سلامةٍ للتأكد من أن شيفرتك البرمجية لا تفعل شيئًا خاطئًا، حيث يمكن إجراء عمليات التحقق من السلامة من خلال استخدام التعليمة assert، وإذا فشل التحقق من السلامة، فسيُرفَع الاستثناء AssertionError. تتكون التعليمة assert مما يلي في الشيفرة البرمجية:

  • الكلمة المفتاحية assert.
  • شرط (أيّ تعبير يمكن تقييمه بالقيمة True أو False).
  • فاصلة.
  • سلسلة نصية تُعرَض عندما تكون قيمة الشرط False.

تمثّل التعليمة assert التأكيد على أن الشرط صحيح، وإذا لم يكن الأمر كذلك، فلا بد من وجود خطأٍ في مكانٍ ما، لذا يجب إيقاف البرنامج مباشرةً. أدخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.sort()
>>> ages
[15, 17, 22, 26, 47, 54, 57, 73, 80, 92]
>>> assert
ages[0] <= ages[-1] # تأكيد أن العمر الأول <= العمر الأخير

تؤكّد التعليمة assert في المثال السابق على أن العنصر الأول في القائمة ages يجب أن يكون أصغر من العنصر الأخير أو يساويه، ويمثّل ذلك التحقق من السلامة، حيث إذا كانت الشيفرة البرمجية الموجودة في التابع ‎sort()‎ خالية من الأخطاء وأدت عملها، فسيكون التأكيد صحيحًا.

يُقيَّم التعبير ages[0] <= ages[-1]‎ على أنه True، وبالتالي لن تفعل التعليمة assert شيئًا، ولكن لنتظاهر بوجود خطأ في شيفرتنا البرمجية، ولنفترض أننا استدعينا عن طريق الخطأ تابعَ القائمة reverse()‎ بدلًا من تابع القائمة sort()‎، حيث سترفع التعليمة assert خطأ AssertionError مثلًا عندما ندخِل ما يلي في الصدفة التفاعلية:

>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.reverse()
>>> ages
[73, 47, 80, 17, 15, 22, 54, 92, 57, 26]
>>> assert ages[0] <= ages[-1] # تأكيد أن العمر الأول <= العمر الأخير
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

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

تُعَد التأكيدات مُخصَّصة لأخطاء المبرمج وليست خاصة بأخطاء المستخدم، إذ يجب أن تفشل التأكيدات فقط عندما يكون البرنامج قيد التطوير، ويجب ألّا يرى المستخدم أبدًا خطأ تأكيد في البرنامج النهائي، لذا يجب أن ترفع استثناءً بدلًا من اكتشافه باستخدام التعليمة assert بالنسبة للأخطاء التي يمكن أن يتعرض لها برنامجك كجزءٍ عادي من عمله مثل عدم العثور على ملف أو إدخال المستخدم بيانات غير صالحة، ويجب ألّا تستخدم تعليمات assert بدلًا من رفع الاستثناءات، لأنه يمكن للمستخدمين اختيار إيقاف التأكيدات. إذا شغّلتَ سكربت بايثون باستخدام الأمر python -O myscript.py بدلًا من الأمر python myscript.py، فستتخطّى شيفرة بايثون تعليمات assert، وقد يعطّل المستخدمون التأكيدات عندما يطورون برنامجًا ويحتاجون إلى تشغيله في بيئة الإنتاج التي تتطلب أداءً أعلى، بالرغم من أنهم في كثير من الحالات سيتركون التأكيدات مفعّلة حتى في ذلك الوقت.

لا تُعَد التأكيدات بديلًا عن الاختبار الشامل، فمثلًا إذا ضبطنا القائمة ages في المثال السابق على القيمة [10, 3, 2, 1, 20]، فلن تلاحظ تعليمة التأكيد assert ages[0] <= ages[-1]‎ أن القائمة غير مرتبة، لأنها ترى أن العمر الأول أصغر من أو يساوي العمر الأخير، وهو الشيء الوحيد الذي تتحقق منه تعليمة التأكيد.

استخدام التأكيد في برنامج لمحاكاة إشارات المرور

لنفترض أنك تنشئ برنامجًا لمحاكاة إشارات المرور، حيث يكون هيكل البيانات الذي يمثّل إشارات التوقف عند التقاطع هو قاموس له المفاتيح 'ns' و 'ew' لإشارات التوقف التي تمثّل جهة الشمال-الجنوب وجهة الشرق-الغرب على التوالي. ستكون القيم الموجودة في هذه المفاتيح إحدى السلاسل النصية 'green' أو 'yellow' أو 'red'، حيث ستبدو الشيفرة البرمجية كما يلي:

market_2nd = {'ns': 'green', 'ew': 'red'}
mission_16th = {'ns': 'red', 'ew': 'green'}

يمثّل المتغيران السابقان تقاطعات شارع السوق Market Street والشارع الثاني 2nd Street، وشارع ميشن Mission Street والشارع السادس عشر 16th Street. نبدأ المشروع من خلال كتابة الدالة ‎switchLights()‎ التي تأخذ قاموسًا يمثّل التقاطع بوصفه وسيطًا وتبّدل بين الأضواء.

نعتقد في البداية أن الدالة ‎switchLights()‎ يجب أن تحوّل ببساطة كل ضوء إلى اللون التالي في السلسلة، حيث يجب أن تتغيّر جميع القيم 'green' إلى القيمة 'yellow'، ويجب أن تتغير قيم 'yellow' إلى القيم 'red'، ويجب أن تتغير القيم 'red' إلى القيم 'green'، إذ قد تبدو الشيفرة البرمجية لتطبيق هذه الفكرة كما يلي:

def switchLights(stoplight):
    for key in stoplight.keys():
        if stoplight[key] == 'green':
            stoplight[key] = 'yellow'
        elif stoplight[key] == 'yellow':
            stoplight[key] = 'red'
        elif stoplight[key] == 'red':
            stoplight[key] = 'green'

switchLights(market_2nd)

لا بد أنك رأيتَ مشكلة هذه الشيفرة البرمجية، ولكن لنفترض أنك كتبتَ بقية شيفرة المحاكاة التي يبلغ طولها آلاف الأسطر دون أن تلاحظ ذلك، حيث لن يتعطل البرنامج عندما تشغّل المحاكاة في النهاية، ولكن ستتعطّل سياراتك الافتراضية في البرنامج. لن يكون لديك أيّ فكرة عن مكان وجود الخطأ بما أنك كتبتَ بقية البرنامج فعليًا، إذ قد يكون الخطأ في الشيفرة البرمجية التي تحاكي السيارات أو في الشيفرة البرمجية التي تحاكي السائقين الافتراضيين، وبالتالي قد يستغرق الأمر ساعات لتعقّب الخطأ العكسي إلى الدالة switchLights()‎.

إذا أضفتَ‎ تأكيدًا أثناء كتابة الدالة switchLights()‎ للتحقّق من أن أحد الأضواء يكون دائمًا باللون الأحمر على الأقل، فيمكن تضمين ما يلي في نهاية الدالة:

assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight)

سيتعطّل برنامجك مع ظهور رسالة الخطأ التالية باستخدام التأكيد السابق:

   Traceback (most recent call last):
     File "carSim.py", line 14, in <module>
       switchLights(market_2nd)
     File "carSim.py", line 13, in switchLights
       assert 'red' in stoplight.values(), 'Neither light is red! ' +
   str(stoplight)
➊ AssertionError: Neither light is red! {'ns': 'yellow', 'ew': 'green'}

السطر المهم في المثال السابق هو AssertionError ➊، حيث لا يُعَد تعطّل برنامجك أمرًا مثاليًا، ولكنه يشير مباشرةً إلى فشل التحقق من السلامة، إذ لا يحتوي أي من اتجاهي حركة المرور على ضوء أحمر، مما يعني أن حركة المرور يمكن أن تسير في كلا الاتجاهين من التقاطع. يمكنك توفير الكثير من جهد تنقيح الأخطاء مستقبلًا من خلال الفشل السريع في وقتٍ مبكر من تنفيذ البرنامج.

التسجيل Logging

إذا سبق لك أن وضعتَ التعليمة print()‎ في شيفرتك البرمجية لإنتاج قيمة بعض المتغيرات أثناء تشغيل البرنامج، فلا بد أنك استخدمتَ صيغة تسجيلٍ لتنقيح أخطاء شيفرتك البرمجية، حيث يُعَد التسجيل طريقةً رائعة لفهم ما يحدث في برنامجك وترتيب حدوثه. تسهّل الوحدة logging في بايثون إنشاء سجلٍ للرسائل المُخصَّصة التي تكتبها، إذ توضّح هذه الرسائل وقت وصول تنفيذ البرنامج إلى استدعاء دالة التسجيل وتسرد المتغيرات التي حدّدتها في ذلك الوقت. بينما تشير رسالة السجل الناقصة إلى تخطي جزء من الشيفرة البرمجية وعدم تنفيذه مطلقًا.

استخدام الوحدة logging

يمكن تفعيل الوحدة logging لعرض رسائل السجل على شاشتك أثناء تشغيل البرنامج من خلال نسخ ما يلي إلى بداية برنامجك، ولكن ضمّن السطر Shebang الذي هو ‎#! python:

import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s -  %(levelname)
s -  %(message)s')

لا داعي للقلق كثيرًا بشأن كيفية عمل السطر السابق، ولكنه ينشئ الكائن LogRecord الذي يحتوي على معلومات حول حدثٍ ما عندما تسجّل بايثون هذا الحدث. تتيح لك الدالة basicConfig()‎ الخاصة بالوحدة logging تحديدَ التفاصيل المتعلقة بكائن LogRecord الذي تريد رؤيته وكيفية عرض هذه التفاصيل.

لنفترض أنك كتبتَ دالةً لحساب عاملي Factorial عددٍ ما، حيث يكون عاملي العدد 4 في الرياضيات هو 1‎ × 2 × 3 × 4 أو القيمة 24 وعاملي العدد 7 هو 1‎ × 2 × 3 × 4 × 5 × 6 × 7 أو القيمة 5040. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد وأدخِل الشيفرة البرمجية التالية التي تحتوي على خطأ، ولكنك ستدخِل أيضًا عددًا من رسائل السجل لمساعدة نفسك في اكتشاف الخطأ الذي يحدث، واحفظ البرنامج بالاسم factorialLog.py:

import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s -  %(levelname)s
-  %(message)s')
logging.debug('Start of program')

def factorial(n):
    logging.debug('Start of factorial(%s%%)'  % (n))
    total = 1
    for i in range(n + 1):
        total *= i
        logging.debug('i is ' + str(i) + ', total is ' + str(total))
    logging.debug('End of factorial(%s%%)'  % (n))
    return total

print(factorial(5))
logging.debug('End of program')

نستخدم الدالة logging.debug()‎ عندما نريد طباعة معلومات السجل، وتستدعي الدالةُ debug()‎ الدالةَ basicConfig()‎ وستطبع سطرًا من المعلومات، حيث ستكون هذه المعلومات بالتنسيق الذي حددناه في الدالة basicConfig()‎ وستتضمّن الرسائل التي مرّرناها إلى الدالة debug()‎. يُعَد استدعاء الدالة print(factorial(5))‎ جزءًا من البرنامج الأصلي، لذا ستُعرَض النتيجة حتى لو تعطّلت رسائل التسجيل.

يبدو خرج هذا البرنامج كما يلي:

2024-05-23 16:20:12,664 - DEBUG - Start of program
2024-05-23 16:20:12,664 - DEBUG - Start of factorial(5)
2024-05-23 16:20:12,665 - DEBUG - i is 0, total is 0
2024-05-23 16:20:12,668 - DEBUG - i is 1, total is 0
2024-05-23 16:20:12,670 - DEBUG - i is 2, total is 0
2024-05-23 16:20:12,673 - DEBUG - i is 3, total is 0
2024-05-23 16:20:12,675 - DEBUG - i is 4, total is 0
2024-05-23 16:20:12,678 - DEBUG - i is 5, total is 0
2024-05-23 16:20:12,680 - DEBUG - End of factorial(5)
0
2024-05-23 16:20:12,684 - DEBUG - End of program

تعيد الدالة factorial()‎ القيمة 0 بوصفها ناتج عاملي العدد 5، وهذا ليس صحيحًا، حيث يجب أن تضرب حلقة for القيمة الموجودة في المتغير total بالأعداد من 1 إلى 5، ولكن توضّح رسائل السجل التي تعرضها الدالة logging.debug()‎ أن المتغير i يبدأ من القيمة 0 بدلًا من القيمة 1، وبالتالي تكون قيمة بقية التكرارات خاطئة للمتغير total أيضًا، لأن ضرب الصفر بأيّ شيء يساوي صفرًا. توفّر رسائل التسجيل سلسلةً من مسارات التنقل التي يمكن أن تساعدك في معرفة متى بدأت الأمور تسوء.

غيّر السطر for i in range(n + 1):‎ إلى for i in range(1, n + 1):‎، وشغّل البرنامج مرة أخرى، وسيبدو الخرج كما يلي:

2024-05-23 17:13:40,650 - DEBUG - Start of program
2024-05-23 17:13:40,651 - DEBUG - Start of factorial(5)
2024-05-23 17:13:40,651 - DEBUG - i is 1, total is 1
2024-05-23 17:13:40,654 - DEBUG - i is 2, total is 2
2024-05-23 17:13:40,656 - DEBUG - i is 3, total is 6
2024-05-23 17:13:40,659 - DEBUG - i is 4, total is 24
2024-05-23 17:13:40,661 - DEBUG - i is 5, total is 120
2024-05-23 17:13:40,661 - DEBUG - End of factorial(5)
120
2024-05-23 17:13:40,666 - DEBUG - End of program

يؤدي استدعاء الدالة factorial(5)‎ إلى إعادة القيمة 120 الصحيحة، وتظهِر رسائل السجل ما يحدث ضمن الحلقة، مما يقودك إلى الخطأ مباشرةً. يمكنك أن ترى أن استدعاءات الدالة logging.debug()‎ لا تطبع السلاسل النصية المُمرَّرة إليها فقط، بل تطبع أيضًا العلامة الزمنية Timestamp والكلمة DEBUG.

لا تنقح الأخطاء باستخدام الدالة print()‎

تُعَد كتابة التعليمتين import logging و logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')‎ أمرًا صعبًا بعض الشيء، لذا سترغب في استخدام استدعاءات الدالة ‎print()‎ بدلًا من ذلك، ولكن لا تنخدع بسهولة استخدام هذه الدالة، لأنك ستقضي كثيرًا من الوقت في إزالة استدعاءات الدالة ‎print()‎ من شيفرتك البرمجية لجميع رسائل السجل بعد الانتهاء من تنقيح الأخطاء، وقد تزيل أيضًا عن طريق الخطأ بعض استدعاءات الدالة ‎print()‎ المُستخدَمة للرسائل التي ليس لها علاقة بالسجل. يمكنك ملء برنامجك بالعدد الذي تريده من رسائل السجل، ويمكنك دائمًا تعطيلها لاحقًا من خلال إضافة استدعاء واحد للدالة logging.disable(logging.CRITICAL)‎، حيث تسهّل الوحدة logging التبديل بين إظهار وإخفاء رسائل السجل على عكس الدالة ‎print()‎.

تُعَد رسائل السجل خاصةً بالمبرمج وليست خاصة بالمستخدم، إذ لن يهتم المستخدم بمحتويات بعض قيم القاموس التي تحتاج إلى رؤيتها للمساعدة في تنقيح الأخطاء، لذا استخدم رسالة سجل لذلك، ولكن يجب أن تستخدم استدعاء الدالة ‎print()‎ بالنسبة للرسائل التي يرغب المستخدم في رؤيتها مثل "عدم العثور على ملف File not found" أو "قيمة إدخال غير صالحة لذا أدخِل قيمة عددية من فضلك Invalid input, please enter a number"، فلن ترغب في حرمان المستخدم من المعلومات المفيدة له بعد تعطيل رسائل السجل.

مستويات التسجيل

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

مستوى التسجيل دالة التسجيل وصفها
المستوى DEBUG الدالة logging.debug()‎ المستوى الأدنى، ويُستخدَم للتفاصيل الصغيرة، حيث تهتم بهذه الرسائل عند تشخيص المشاكل فقط.
المستوى INFO الدالة logging.info()‎ يُستخدَم لتسجيل معلومات عن الأحداث العامة في برنامجك أو للتأكد من أن الأمور تسير جيدًا في البرنامج.
المستوى WARNING الدالة logging.warning()‎ يُستخدم للإشارة إلى مشكلة محتملة لا تمنع البرنامج من العمل ولكنها قد تفعل ذلك مستقبلًا.
المستوى ERROR الدالة logging.error()‎ يُستخدم لتسجيل خطأٍ تسبّب في فشل البرنامج بعمل شيءٍ ما.
المستوى CRITICAL الدالة logging.critical()‎ المستوى الأعلى، ويُستخدَم للإشارة إلى خطأ كبير تسبّب أو أنه على وشك التسبّب في توقف البرنامج عن العمل بالكامل.

تُمرَّر رسالة التسجيل بوصفها سلسلة نصية إلى هذه الدوال. تُعَد مستويات التسجيل مجرد اقتراحات، فالأمر متروك لك لتحديد الفئة التي تندرج ضمنها رسالة السجل الخاصة بك. لندخِل الآن ما يلي في الصدفة التفاعلية:

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s -
%(levelname)s -  %(message)s')
>>> logging.debug('Some debugging details.')
2024-05-18 19:04:26,901 - DEBUG - Some debugging details.
>>> logging.info('The logging module is working.')
2024-05-18 19:04:35,569 - INFO - The logging module is working.
>>> logging.warning('An error message is about to be logged.')
2024-05-18 19:04:56,843 - WARNING - An error message is about to be logged.
>>> logging.error('An error has occurred.')
2024-05-18 19:05:07,737 - ERROR - An error has occurred.
>>> logging.critical('The program is unable to recover!')
2024-05-18 19:05:45,794 - CRITICAL - The program is unable to recover!

تتمثّل فائدة مستويات التسجيل في أنه يمكنك تغيير أولوية رسالة التسجيل التي تريد رؤيتها، حيث سيؤدي تمرير المستوى logging.DEBUG إلى وسيط الكلمة المفتاحية Keyword Argument الذي هو level الخاص بالدالة basicConfig()‎ إلى إظهار الرسائل من جميع مستويات التسجيل، حيث يُعَد المستوى DEBUG هو المستوى الأدنى. قد تكون مهتمًا بالأخطاء فقط بعد تطوير برنامجك، وبالتالي يمكنك ضبط الوسيط level الخاص بالدالة basicConfig()‎ على المستوى logging.ERROR في هذه الحالة، إذ سيعرض هذا المستوى رسائل المستوى ERROR ورسائل المستوى CRITICAL فقط ويتخطى رسائل المستويات DEBUG و INFO و WARNING.

تعطيل التسجيل

لا بد أنك تفضّل عدم إظهار جميع رسائل السجل التي تؤدي إلى جعل الشاشة مزدحمة بعد تنقيح أخطاء برنامجك، لذا توجد الدالة logging.disable()‎ التي تعمل على تعطيل رسائل السجل حتى لا تضطر إلى الدخول إلى برنامجك وإزالة جميع استدعاءات التسجيل يدويًا. يمكنك تمرير هذه الدالة إلى مستوى التسجيل فقط، وستمنع هذه الدالة جميع رسائل السجل عند هذا المستوى أو المستويات الأقل، لذا إذا أردتَ تعطيل التسجيل بالكامل، فما عليك سوى إضافة الاستدعاء logging.disable(logging.CRITICAL)‎ إلى برنامجك. أدخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> import logging
>>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s -
%(levelname)s -  %(message)s')
>>> logging.critical('Critical error! Critical error!')
2024-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error!
>>> logging.disable(logging.CRITICAL)
>>> logging.critical('Critical error! Critical error!')
>>> logging.error('Error! Error!')

ستعطّل الدالة ‎logging.disable()‎ جميع الرسائل بعدها، لذا يُحتمَل أنك تريد إضافتها بالقرب من سطر الاستيراد import logging في برنامجك، وبالتالي يمكنك بسهولة العثور عليه لتعليق هذا الاستدعاء أو إلغاء تعليقه لتفعيل رسائل التسجيل أو تعطيلها حسب الحاجة.

التسجيل في ملف

يمكنك كتابة رسائل السجل في ملف نصي بدلًا من عرضها على الشاشة، حيث تأخذ الدالة logging.basicConfig()‎ وسيط الكلمة المفتاحية filename كما يلي:

import logging
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='
%(asctime)s -  %(levelname)s -  %(message)s')

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

منقح أخطاء المحرر Mu

يُعَد منقّح الأخطاء من ميزات المحرّر Mu و IDLE وبرامج تحرير النصوص الأخرى التي تسمح لك بتنفيذ برنامجك من خلال تنفيذ سطر واحد في كل مرة، حيث يشغّل منقّح الأخطاء سطرًا واحد من الشيفرة البرمجية ثم ينتظر حتى تخبره بالمتابعة. يمكنك أن تأخذ الوقت الذي تريده لفحص القيم الموجودة في المتغيرات في أي مرحلة معينة من عمر البرنامج من خلال تشغيل برنامجك مع منقّح الأخطاء باستخدام هذه الطريقة، إذ يُعَد منقّح الأخطاء أداةً مفيدة لتعقّب الأخطاء.

يمكنك تشغيل برنامج مع منقّح أخطاء المحرّر Mu من خلال النقر على زر تنقيح الأخطاء "Debug" في الصف العلوي من الأزرار بجانب زر التشغيل "Run". ستفتح نافذة فحص تنقيح الأخطاء "Debug Inspector" على طول الجانب الأيمن من النافذة بالإضافة إلى نافذة الخرج المعتاد في الأسفل، حيث تسرد نافذة فحص تنقيح الأخطاء قيم المتغيرات الحالية في برنامجك. يوقِف منقّح الأخطاء في الشكل التالي تنفيذ البرنامج قبل تشغيل السطر الأول من الشيفرة البرمجية، حيث يمكنك رؤية هذا السطر مميزًا في محرّر الملفات:

01 000049

تشغيل المحرّر Mu لبرنامجٍ ما مع منقّح الأخطاء

يضيف وضع تنقيح الأخطاء أيضًا أزرارًا جديدة إلى أعلى المحرّر وهي: زر المتابعة "Continue" وزر "Step Over" وزر "Step In" وزر "Step Out"، ويوجد زر التوقف "Stop" المعتاد أيضًا.

زر المتابعة Continue

يؤدي النقر على زر المتابعة "Continue" إلى تنفيذ البرنامج بطريقة طبيعية حتى ينتهي البرنامج أو يصل إلى نقطة توقف Breakpoint (سنوضح نقاط التوقف لاحقًا في هذا المقال). إذا انتهيتَ من تنقيح الأخطاء وأردتَ أن يتابع البرنامج عمله بطريقة طبيعية، فانقر على زر المتابعة "Continue".

زر Step In

يؤدي النقر على زر "Step In" إلى أن ينفّذ منقّح الأخطاء السطر التالي من الشيفرة البرمجية ثم يوقفه مرةً أخرى. إذا كان السطر التالي من الشيفرة البرمجية هو استدعاء دالة، فسيدخل منقّح الأخطاء إلى تلك الدالة وينتقل إلى السطر الأول من الشيفرة البرمجية فيها.

زر Step Over

يؤدي النقر على زر "Step Over" إلى تنفيذ السطر التالي من الشيفرة البرمجية مثل زر "Step In"، ولكن إذا كان السطر التالي من الشيفرة البرمجية هو استدعاء دالة، فسيتجاوز زر "Step Over" الشيفرة البرمجية الموجودة في هذه الدالة، حيث ستُنفَّذ الشيفرة البرمجية الخاصة بالدالة بالسرعة القصوى، وسيتوقّف منقّح الأخطاء بعد العودة من استدعاء هذه الدالة. إذا استدعى السطر التالي من الشيفرة البرمجية الدالة spam()‎ مثلًا، ولكنك لا تهتم بالشيفرة البرمجية الموجودة ضمن هذه الدالة، فيمكنك النقر على زر "Step Over" لتنفيذ الشيفرة البرمجية الموجودة في الدالة بالسرعة العادية ثم التوقف عندما تعود الدالة، لذلك يُعَد استخدام زر "Step Over" أكثر شيوعًا من استخدام زر "Step In".

زر Step Out

يؤدي النقر على زر "Step Out" إلى أن ينفّذ منقّح الأخطاء سطورًا من الشيفرة البرمجية بالسرعة القصوى حتى العودة من الدالة الحالية. إذا دخلتَ في استدعاء دالة باستخدام زر "Step In" وتريد الاستمرار في تنفيذ التعليمات حتى الخروج منها، فانقر على زر "Step Out" للخروج من استدعاء الدالة الحالي.

زر التوقف Stop

إذا أردتَ إيقاف تنقيح الأخطاء وعدم إزعاجك بمتابعة تنفيذ بقية البرنامج، فانقر فوق زر التوقّف "Stop"، حيث سيؤدي الزر "Stop" إلى إنهاء البرنامج مباشرةً.

تنقيح أخطاء برنامج لجمع الأعداد

افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد وأدخِل الشيفرة البرمجية التالية:

print('Enter the first number to add:')
first = input()
print('Enter the second number to add:')
second = input()
print('Enter the third number to add:')
third = input()
print('The sum is ' + first + second + third)

احفظ البرنامج بالاسم buggyAddingProgram.py وشغّله أولًا دون تفعيل منقّح الأخطاء، وسيكون خرج البرنامج كما يلي:

Enter the first number to add:
5
Enter the second number to add:
3
Enter the third number to add:
42
The sum is 5342

لم يتعطل البرنامج، ولكن من الواضح أن ناتج الجمع خاطئ. شغّل البرنامج مرة أخرى ولكن مع منقّح الأخطاء هذه المرة، حيث إذا نقرتَ على زر تنقيح الأخطاء "Debug"، فسيتوقف البرنامج مؤقتًا عند السطر 1، وهو سطر الشيفرة البرمجية الذي نوشك على تنفيذه، إذ يجب أن يبدو المحرّر Mu كما في الشكل السابق. انقر على زر "Step Over" مرة واحدة لتنفيذ الاستدعاء الأول print()‎، إذ يجب أن تستخدم زر "Step Over" بدلًا من زر "Step In" هنا، لأنك لا تريد الدخول إلى الشيفرة البرمجية الخاصة بالدالة print()‎، بالرغم من أن المحرّر Mu يجب أن يمنع منقّح الأخطاء من الدخول إلى دوال بايثون المُدمَجة. ينتقل منقّح الأخطاء إلى السطر 2، ويميّز السطر 2 في محرر الملفات كما هو موضح في الشكل التالي، مما يوضّح لك مكان تنفيذ البرنامج حاليًا:

02 000144

نافذة المحرّر Mu بعد النقر على زر "Step Over"

انقر على زر "Step Over" مرة أخرى لتنفيذ استدعاء الدالة input()‎، حيث سيختفي التمييز عن الشيفرة البرمجية أثناء انتظار المحرّر Mu أن تكتب شيئًا ما لاستدعاء الدالة input()‎ في نافذة الخرج. أدخِل القيمة 5 واضغط على مفتاح ENTER، ثم سيعود التمييز إلى الشيفرة البرمجية.

استمر في النقر على زر "Step Over"، وأدخِل القيمتين 3 و 42 بوصفهما العددين التاليين. يجب أن تبدو نافذة المحرّر Mu كما في الشكل التالي عندما يصل منقّح الأخطاء إلى السطر 7 الذي يمثّل استدعاء الدالة print()‎ النهائي في البرنامج:

03 000086

توضّح نافذة فحص تنقيح الأخطاء Debug Inspector الموجودة على الجانب الأيمن أن المتغيرات مضبوطة بوصفها سلاسلًا نصية وليست أعدادًا صحيحة، مما تسبّب في حدوث الخطأ

يجب أن ترى في نافذة فحص تنقيح الأخطاء Debug Inspector أن المتغيرات first و second و third مضبوطة بوصفها سلاسلًا نصية '5' و '3' و '42' بدلًا من الأعداد الصحيحة 5 و 3 و 42. تضم لغة بايثون هذه السلاسل النصية مع بعضها بعضًا عند تنفيذ السطر الأخير بدلًا من جمع هذه الأعداد، مما يتسبّب في حدوث الخطأ.

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

نقاط التوقف Breakpoints

يمكن ضبط نقطة توقف على سطر معين من الشيفرة البرمجية وإجبار منقح الأخطاء على التوقف مؤقتًا عندما يصل تنفيذ البرنامج إلى هذا السطر. افتح تبويبًا جديدًا في محرّر الملفات وأدخِل البرنامج التالي الذي يحاكي رمي عملة معدنية 1000 مرة، واحفظ البرنامج بالاسم coinFlip.py:

import random
heads = 0
for i in range(1, 1001):
   if random.randint(0, 1) == 1:
         heads = heads + 1
     if i == 500:
       print('Halfway done!')
print('Heads came up ' + str(heads) + ' times.')

سيعيد الاستدعاء random.randint(0, 1)‎ ➊ القيمة 0 في نصف الوقت والقيمة 1 في النصف الآخر من الوقت، حيث يمكن استخدام هذا الاستدعاء لمحاكاة رمي قطعة نقود وفق الاحتمال 50/50 وتمثل القيمة 1 الصورة من العملة المعدنية. يكون خرج هذا البرنامج كما يلي عند تشغيله بدون منقّح الأخطاء:

Halfway done!
Heads came up 490 times.

إذا شغّلتَ هذا البرنامج مع منقّح الأخطاء، فيجب أن تنقر على زر "Step Over" آلاف المرات قبل إنهاء البرنامج. إذا كنت مهتمًا بالقيمة heads التي تمثّل صورة العملة المعدنية عند منتصف تنفيذ البرنامج أو عند اكتمال 500 مرة من 1000 مرة لرمي قطعة نقود، فيمكنك ضبط نقطة توقف على السطر print('Halfway done!')‎ ➋، حيث يمكنك ضبط نقطة توقف من خلال النقر على رقم السطر في محرّر الملفات بحيث تظهر نقطة حمراء كما في الشكل التالي:

04 000124

يؤدي ضبط نقطة التوقف إلى ظهور نقطة حمراء (محاطة بدائرة) بجانب رقم السطر

لا نريد ضبط نقطة توقف عند سطر التعليمة if لأنها تُنفَّذ في كل تكرار للحلقة، حيث إذا ضبطنا نقطة التوقف على الشيفرة البرمجية الموجودة ضمن التعليمة if، فسيتوقّف منقّح الأخطاء فقط عندما يدخل التنفيذ إلى هذه التعليمة.

سيكون للسطر الذي يحتوي على نقطة التوقف نقطة حمراء بجانبه. إذا شغّلنا البرنامج مع منقّح الأخطاء، فسيبدأ في حالة التوقّف المؤقت عند السطر الأول كالمعتاد، ولكن إذا نقرت على زر المتابعة "Continue"، فسيُشغَّل البرنامج بالسرعة القصوى حتى يصل إلى السطر الذي ضبطنا نقطة التوقف عنده. يمكنك بعد ذلك النقر على أزرار "Continue" أو "Step Over" أو "Step In" أو "Step Out" للمتابعة كالمعتاد.

إذا أردتَ إزالة نقطة توقف، فانقر على رقم السطر مرة أخرى، وستختفي النقطة الحمراء، ولن يتوقف منقّح الأخطاء عند هذا السطر لاحقًا.

مشروع للتدريب

حاول كتابة برنامج يطبّق ما يلي لكسب خبرة عملية أكبر.

تنقيح أخطاء برنامج لرمي عملة معدنية

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

import random
guess = ''
while guess not in ('heads', 'tails'):
    print('Guess the coin toss! Enter heads or tails:')
    guess = input()
toss = random.randint(0, 1) # تمثّل القيمة 0 الكتابة، وتمثل القيمة 1 الصورة في العملة المعدنية
if toss == guess:
    print('You got it!')
else:
    print('Nope! Guess again!')
    guesss = input()
    if toss == guess:
        print('You got it!')
    else:
        print('Nope. You are really bad at this game.')

الخلاصة

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

يمكن اكتشاف الاستثناء ومعالجته باستخدام تعليمات try و except. تُعَد الوحدة logging طريقة جيدة لتفحّص شيفرتك البرمجية أثناء تشغيلها، وهي أكثر ملاءمة للاستخدام من الدالة print()‎ لاحتوائها على مستويات تسجيل متعددة ولقدرتها على التسجيل في ملف نصي.

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

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

ترجمة -وبتصرُّف- للمقال Debugging لصاحبه Al Sweigart.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...