كيف تشخّص أخطاء Python عمليًا بعد قراءة Traceback؟ منهج Debugging خطوة بخطوة

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

في هذا المقال من بايثون العرب سنركز على Debugging في Python بشكل عملي: كيف تعيد إنتاج المشكلة، تعزل الجزء المسبب لها، تفحص القيم والأنواع، تستخدم print() بذكاء، تنتقل إلى breakpoint() عند الحاجة، ثم تتحقق أن الإصلاح حل السبب ولم يخفِ العرض فقط.

{getToc} $title={محتوى المقال}

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

ما معنى Debugging في Python؟

الـ Debugging ليس مجرد تعديل السطر الذي ظهر في Traceback. هو عملية منظمة لاختبار فرضياتك حول الكود حتى تعرف أين بدأت القيمة أو الحالة الخاطئة، ثم تصلح السبب وتعيد الاختبار.

وقد يكون لديك خطأ واضح يوقف البرنامج، أو مشكلة منطقية لا تنتج أي Exception أصلًا. مثل هذا المثال:

def calculate_discount(price, discount_percent):
    return price - discount_percent

print(calculate_discount(200, 20))

الكود يعمل ويطبع 180، لكن إذا كان المقصود خصم 20% فالناتج الصحيح هو 160. هنا لا يوجد Traceback أصلًا؛ المشكلة في منطق الحساب. لذلك التشخيص الجيد لا يعتمد على رسائل الخطأ وحدها.

قبل Debugging: ماذا نأخذ من Traceback فقط؟

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

نوع الخطأ لا يساوي دائمًا السبب الحقيقي

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

لهذا اسأل دائمًا: من أين جاءت هذه القيمة؟ ومتى أصبحت مختلفة عما كنت أتوقع؟

منهج Debugging عملي خطوة بخطوة

1. أعد إنتاج المشكلة بنفس المدخلات

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

  • المدخل: ما القيمة أو الملف أو الحالة التي استخدمتها؟
  • المتوقع: ما الذي كان يفترض أن يحدث؟
  • الفعلي: ماذا حدث أو ماذا طبع البرنامج؟
{alertSuccess} إذا لم تستطع إعادة إنتاج المشكلة، فمن الصعب أن تعرف هل إصلاحك نجح فعلًا أم أن الخطأ لم يظهر هذه المرة فقط.

2. حدد أول قيمة مشبوهة، لا آخر سطر فقط

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

3. قلّل الكود حتى تعزل المشكلة

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

هذا يقودك إلى ما يسمى Minimal Reproducible Example: أصغر مثال ما يزال يعيد إنتاج المشكلة.

def average(numbers):
    return sum(numbers) / len(numbers)

data = []
print(average(data))

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

استخدام print لتصحيح الأخطاء بطريقة مفيدة

استخدام print() مفيد جدًا للمبتدئ، لكن لا تطبع عبارة عامة مثل "وصل هنا" فقط. اطبع اسم المتغير وقيمته ونوعه عند النقطة التي تريد فحصها.

def calculate_total(price, quantity):
    print(f"DEBUG price={price!r}, type={type(price).__name__}")
    print(f"DEBUG quantity={quantity!r}, type={type(quantity).__name__}")
    return price * quantity

print(calculate_total("12.5", 3))

ستلاحظ أن price نص وليس float. لهذا ينتج تكرار النص بدل عملية حسابية صحيحة. الآن لديك فرضية محددة: المشكلة في نوع price قبل الوصول إلى عملية الضرب.

بعد التأكد من المصدر، أصلح التحويل في المكان المناسب:

def calculate_total(price, quantity):
    return float(price) * quantity

print(calculate_total("12.5", 3))

الناتج الآن 37.5. المهم هنا ليس الحل نفسه، بل الطريقة: افحص القيمة → افهم النوع → كوّن فرضية → غيّر شيئًا واحدًا → أعد الاختبار.

لا تغيّر عدة أشياء في الوقت نفسه

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

استخدم دورة صغيرة:

  1. كوّن فرضية واحدة.
  2. غيّر أقل قدر ممكن من الكود.
  3. شغّل نفس حالة الاختبار.
  4. قارن النتيجة بالمتوقع.
  5. إذا فشلت الفرضية، ارجع وتحقق من قيمة أخرى.

متى تنتقل من print إلى breakpoint وDebugger؟

عندما تصبح جمل print() كثيرة، أو تحتاج إلى إيقاف البرنامج في لحظة محددة وفحص عدة متغيرات والتنقل داخل الدوال، استخدم breakpoint(). في Python الحديثة تدخل هذه الدالة افتراضيًا إلى المنقح المدمج pdb.

def calculate_total(price, quantity):
    subtotal = price * quantity
    breakpoint()
    tax = subtotal * 0.15
    return subtotal + tax

print(calculate_total(100, 2))

عند توقف البرنامج، أهم الأوامر التي يحتاجها المبتدئ في pdb هي:

الأمروظيفته
p variableطباعة قيمة متغير.
nتنفيذ السطر الحالي والانتقال إلى السطر التالي داخل الدالة الحالية.
sالدخول إلى الدالة التي يتم استدعاؤها.
cمتابعة التنفيذ حتى نقطة توقف أخرى أو نهاية البرنامج.
qإنهاء جلسة التصحيح.

للتوسع راجع توثيق pdb الرسمي و توثيق breakpoint().

متى أستخدم logging بدل print؟

إذا كانت المشكلة تحدث بعد عدة خطوات، أو داخل برنامج يعمل لفترة طويلة، أو تريد الاحتفاظ بسجل لما حدث، يكون logging أنسب من عشرات جمل print().

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(levelname)s:%(message)s",
)

def calculate_total(price, quantity):
    logging.debug("price=%r quantity=%r", price, quantity)
    return price * quantity

print(calculate_total(12.5, 3))

لا نحول هذا المقال إلى دليل logging كامل؛ لدى بايثون العرب شرح مستقل: شرح logging في Python لتسجيل الأحداث والأخطاء. والتوثيق الرسمي يوضح أن مستوى DEBUG مخصص للمعلومات التفصيلية المفيدة أثناء تشخيص المشكلات.

كيف تبني Minimal Reproducible Example؟

إذا احتجت مساعدة من شخص آخر أو أردت التأكد أنك عزلت السبب، اصنع مثالًا صغيرًا يحقق ثلاثة شروط:

  • Minimal: احذف أي دالة أو import أو بيانات لا تحتاجها لإظهار المشكلة.
  • Reproducible: يجب أن تظهر المشكلة كل مرة بنفس الخطوات.
  • Complete: ضع كل ما يلزم لتشغيل المثال بدون ملفات سرية أو أجزاء مفقودة.

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

اختبر الإصلاح ولا تكتفِ باختفاء الخطأ

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

def safe_average(numbers):
    if not numbers:
        return None
    return sum(numbers) / len(numbers)

assert safe_average([10, 20]) == 15
assert safe_average([]) is None

print("PASS")
{alertWarning} لا تستخدم try/except فقط لإخفاء Exception إذا لم تفهم سببه. معالجة الاستثناء مفيدة عندما تكون الحالة متوقعة وتعرف ماذا تفعل عند وقوعها، وليست بديلًا عن التشخيص.

كيف تبحث عن المشكلة بعد أن تعزلها؟

إذا احتجت للبحث، لا ترسل مشروعك كاملًا ولا تبحث باسم متغير خاص بك. استخدم نوع الخطأ + الجزء العام من الرسالة + السياق التقني، أو شارك Minimal Reproducible Example بعد إزالة البيانات الحساسة.

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

Checklist تشخيص أخطاء Python

الخطوةالسؤال الذي تسألهالأداة المناسبة
إعادة الإنتاجهل أستطيع جعل المشكلة تظهر بنفس المدخلات؟حالة اختبار ثابتة
تحديد المكانأين ظهر الخطأ، وأين بدأت القيمة غير المتوقعة؟Traceback + تتبع القيم
العزلما أقل كود يعيد نفس المشكلة؟Minimal Reproducible Example
فحص القيمما قيمة المتغير ونوعه الآن؟print() وrepr() وtype()
اختبار الفرضيةهل تغيير واحد صغير يغير النتيجة كما توقعت؟تشغيل متكرر
التنقل داخل التنفيذأحتاج مشاهدة القيم سطرًا بسطر؟breakpoint() / pdb
تتبع طويلالمشكلة تظهر بعد وقت أو عبر عدة أجزاء؟logging
التحققهل نجحت الحالة التي كانت تفشل ولم تنكسر الحالات الأخرى؟assert أو اختبارات مناسبة

مثال كامل: من العرض إلى السبب الجذري

لنفترض أن برنامجًا يطبع قيمة غير صحيحة بدل أن يتوقف. لا تبدأ بإعادة كتابة الدالة. افحص البيانات أولًا:

def calculate_total(price, quantity):
    return price * quantity

price_from_input = "12.5"

print(f"price={price_from_input!r}, type={type(price_from_input).__name__}")

price = float(price_from_input)
total = calculate_total(price, 3)

assert total == 37.5
print(total)

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

الخلاصة

بعد أن تفهم Traceback، لا تغيّر الكود عشوائيًا. أعد إنتاج المشكلة، ابحث عن أول قيمة أو حالة خرجت عن المتوقع، قلّل الكود حتى تعزل السبب، ثم استخدم print() أو breakpoint() أو logging حسب حجم المشكلة.

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

{alertSuccess} الخلاصة العملية: Reproduce → Isolate → Inspect → Hypothesis → One Change → Verify.

إرسال تعليق

أحدث أقدم