كيفية قراءة رسائل تتبّع الأخطاء

نظرة عامة على كيفية قراءة تتبّعات Python لتصحيح الأخطاء.


كائن الإطار

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

مكدس الاستدعاءات

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

التتبّع

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

كيف تقرأ التتبّع

ValueError استثناء شائع.

فيما يلي مثال على ValueError ناتج عن محاولة إسناد متغيرين على اليسار انطلاقًا من قيمة واحدة فقط على اليمين:

>>> first, second = [1]
Traceback (most recent call last):
File <stdin>, line 1, in <module>
    first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)

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

>>> def my_func():
...     first, second = [1]
...
>>> my_func()
Traceback (most recent call last):
  File <stdin>, line 5, in <module>
    my_func()
  File <stdin>, line 2, in my_func
    first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)

وبالقراءة من الأسفل إلى الأعلى، نرى أن الاستدعاء الذي وقع فيه الاستثناء في السطر 2 داخل my_func. وقد وصلنا إلى هناك باستدعاء my_func في السطر 5.

الاستثناءات الشائعة

يعرّف Python أكثر من 60 صنفًا من أصناف الاستثناءات المدمجة. فيما يلي نظرة موجزة على بعض الاستثناءات الأكثر شيوعًا وما تشير إليه.

SyntaxError

يرفع Python استثناء SyntaxError عندما يعجز عن فهم الكود بسبب صياغة غير صالحة. على سبيل المثال، قد يكون هناك قوس هلالي مفتوح دون قوس هلالي مغلق يقابله.

اضغط هنا للحصول على مثال برمجي.

تشغيل هذا الكود:

def distance(strand_a, strand_b):
    if len(strand_a) != len(strand_b):
        raise ValueError("Strands must be of equal length."  # This is missing the closing parenthesis

سيؤدي إلى تتبّع مكدس شبيه بهذا (لاحظ الرسالة في السطر الأخير):

.usr.local.lib.python3.10.site-packages._pytest.python.py:608: in _importtestmodule
    mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
.usr.local.lib.python3.10.site-packages._pytest.pathlib.py:533: in import_path
    importlib.import_module(module_name)
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1050: in _gcd_import ???
<frozen importlib._bootstrap>:1027: in _find_and_load ???
<frozen importlib._bootstrap>:1006: in _find_and_load_unlocked ???
<frozen importlib._bootstrap>:688: in _load_unlocked ???
.usr.local.lib.python3.10.site-packages._pytest.assertion.rewrite.py:168: in exec_module
    exec(co, module.__dict__)
.mnt.exercism-iteration.hamming_test.py:3: in <module>
    from hamming import (  
E     File ".mnt.exercism-iteration.hamming.py", line 10
E       raise ValueError("Strands must be of equal length."
E                       ^
E   SyntaxError: '(' was never closed

AssertionError

يرفع Python استثناء AssertionError عندما تفشل عبارة assert (انظر أدناه).

اضغط هنا للحصول على مثال برمجي.

تشغيل هذا الكود:

def distance(strand_a, strand_b):
    assert len(strand_a) == len(strand_b)


distance("ab", "abc")

سيؤدي إلى تتبّع مكدس شبيه بهذا (لاحظ الرسالة في السطر الأخير):

hamming_test.py:3: in <module>
    from hamming import (
hamming.py:5: in <module>
    distance("ab", "abc")
hamming.py:2: in distance
    assert len(strand_a) == len(strand_b)
E   AssertionError

AttributeError

يُرفع استثناء AttributeError عندما يحاول الكود (أو اختبار وحدة!) الوصول إلى سمة كائن لا يملك تلك السمة. على سبيل المثال، يتوقع اختبار وحدة أن يملك كائن Robot سمة direction، لكنه حين حاول الوصول إلى robot.direction وجدها غير موجودة.

قد يشير هذا أيضًا إلى خطأ مطبعي، مثل استخدام "Hello".lowercase() بينما الصياغة الصحيحة هي "Hello".lower(). يرفع "Hello".lowercase() الخطأ AttributeError: 'str' object has no attribute 'lowercase'.

اضغط هنا للحصول على مثال لصنف Robot.

تشغيل هذا الكود:

class Robot:
    def __init__():
        #note that there is no self.direction listed here
        self.position = (0, 0)
        self.orientation = 'SW'

    def forward():
        pass
        

robby = Robot
robby.direction

سيؤدي إلى تتبّع مكدس شبيه بهذا (لاحظ الرسالة في السطر الأخير):

robot_simulator_test.py:3: in <module>
    from robot_simulator import (
robot_simulator.py:12: in <module>
    robby.direction
E   AttributeError: type object 'Robot' has no attribute 'direction'
اضغط هنا للحصول على مثال لسلسلة نصية.

تشغيل هذا الكود:

def distance(strand_a, strand_b):
    if strand_a.lowercase() == strand_b:
        return 0


distance("ab", "abc")

سيؤدي إلى تتبّع مكدس شبيه بهذا (لاحظ الرسالة في السطر الأخير):

    def distance(strand_a, strand_b):
>       if strand_a.lowercase() == strand_b:
E       AttributeError: 'str' object has no attribute 'lowercase'

ImportError

يُرفع استثناء ImportError عندما يحاول الكود استيراد شيء ما، لكن Python يعجز عن ذلك. على سبيل المثال، ينفّذ اختبار وحدة للتمرين Guidos Gorgeous Lasagna الأمر from lasagna import bake_time_remaining، لكن ملف الحل lasgana.py قد لا يعرّف bake_time_remaining.

اضغط هنا للحصول على مثال برمجي

تشغيل ملف lasgana.py دون الدالة المعرَّفة سيؤدي إلى الخطأ التالي:

We received the following error when we ran your code:

  ImportError while importing test module '.mnt.exercism-iteration.lasagna_test.py'.
Hint: make sure your test modules.packages have valid Python names.

Traceback:
.mnt.exercism-iteration.lasagna_test.py:6: in <module>
    from lasagna import (EXPECTED_BAKE_TIME,
E   ImportError: cannot import name 'bake_time_remaining' from 'lasagna' (.mnt.exercism-iteration.lasagna.py)

During handling of the above exception, another exception occurred:
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
.mnt.exercism-iteration.lasagna_test.py:23: in <module>
    raise ImportError("In your 'lasagna.py' file, we can not find or import the"

E   ImportError: In your 'lasagna.py' file, we can not find or import the function named 'bake_time_remaining()'. Did you mis-name or forget to define it?

### **IndexError**

Python raises an `IndexError` when an invalid index is used to look up a value in a sequence.
This often indicates the index is not computed properly and is often an off-by-one error.


<details>
<summary>Click here for code example</summary>


Consider the following code.

```python
def distance(strand_a, strand_b):
    same = 0
    for i in range(len(strand_a)):
        if strand_a[i] == strand_b[i]:
            same += 1
    return same


distance("abc", "ab")  # Note the first strand is longer than the second strand.

سيؤدي تشغيل هذا الكود إلى خطأ شبيه بهذا. (لاحظ السطر الأخير.)

hamming_test.py:3: in <module>
    from hamming import (
hamming.py:9: in <module>
    distance("abc", "ab")  # Note the first strand is longer than the second strand.
hamming.py:4: in distance
    if strand_a[i] == strand_b[i]:
E   IndexError: string index out of range

KeyError

على غرار IndexError، يُرفع هذا الاستثناء عند استخدام مفتاح للبحث عن قيمة في قاموس، لكن المفتاح غير موجود في القاموس.

اضغط هنا للحصول على مثال برمجي

تأمل الكود التالي.

def to_rna(dna_letter):
    translation = {"G": "C", "C": "G", "A": "U", "T": "A"}
    return translation[dna_letter]


print(to_rna("Q"))  # Note, "Q" is not in the translation.

سيؤدي تشغيل هذا الكود إلى خطأ شبيه بهذا. (لاحظ السطر الأخير.)

rna_transcription_test.py:3: in <module>
    from rna_transcription import to_rna
rna_transcription.py:6: in <module>
    print(to_rna("Q"))
rna_transcription.py:3: in to_rna
    return translation[dna_letter]
E   KeyError: 'Q'

TypeError

عادةً يُرفع استثناء TypeError عند تمرير نوع بيانات خاطئ إلى دالة أو استخدامه في عملية.

اضغط هنا للحصول على مثال برمجي

تأمل الكود التالي.

def hello(name):  # This function expects a string.
    return 'Hello, ' + name + '!'


print(hello(100))  # 100 is not a string.

سيؤدي تشغيل هذا الكود إلى خطأ شبيه بهذا. (لاحظ السطر الأخير.)

hello_world_test.py:3: in <module>
    import hello_world
hello_world.py:5: in <module>
    print(hello(100))
hello_world.py:2: in hello
    return 'Hello, ' + name + '!'
E   TypeError: can only concatenate str (not "int") to str

ValueError

عادةً يُرفع استثناء ValueError عند تمرير قيمة غير صالحة إلى دالة.

اضغط هنا للحصول على مثال برمجي

لاحظ أن الجذور التربيعية الحقيقية لا توجد إلا للأعداد الموجبة. استدعاء math.sqrt(-1) سيرفع الخطأ ValueError: math domain error لأن -1 ليست قيمة صالحة للجذر التربيعي. وبالمصطلحات التقنية (الرياضية)، فإن -1 ليست ضمن مجال الجذور التربيعية.

import math

math.sqrt(-1)

سيؤدي تشغيل هذا الكود إلى خطأ شبيه بهذا. (لاحظ السطر الأخير.)

square_root_test.py:3: in <module>
    from square_root import (
square_root.py:3: in <module>
    math.sqrt(-1)
E   ValueError: math domain error

استخدام الدالة print

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

# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
    return (num / 2) * 4

عند تمرير 5 إلى الدالة، تكون القيمة المتوقعة 8، لكنها تُرجع 10.0. لمعالجة المشكلة، نُقسّم الحساب بحيث يمكن فحص القيمة عند كل خطوة.

# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
    # verify the number in is what is expected
    # prints 5
    print(num)
    # we want the int divided by an integer to be an integer
    # but this prints 2.5! We've found our mistake.
    print(num / 2)
    # this makes sense, since 2.5 x 4 = 10.0
    print((num / 2) * 4)
    return (num / 2) * 4

What the `print` calls revealed is that we used `/` when we should have used `//`, the [floor division operator][floor division operator].

## Logging

[Logging][logging] can be used similarly to `print`, but it is more powerful.
What is logged can be configured by the logging severity (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.)
A call to the `logging.error` function can pass `True` to the `exc_info` parameter, which will additionally log the stack trace.
By configuring multiple handlers, logging can write to more than one place with the same logging function.

Following is an example of logging printed to the console:

```python

>>> import logging
>>>
>>> # configures minimum logging level as INFO
>>> logging.basicConfig(level=logging.INFO)
>>>
>>> def halve_and_quadruple(num):
...     # prints INFO:root: num == 5
...     logging.info(f" num == {num}")
...     return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))

ضُبط المستوى على INFO لأن المستوى الافتراضي هو WARNING. وبالنسبة إلى سجل دائم، يمكن ضبط المسجّل ليكتب إلى ملف، هكذا:

>>> import logging
...
>>> # configures the output file name to example.log, and the minimum logging level as INFO
>>> logging.basicConfig(filename='example.log', level=logging.INFO)
...
... def halve_and_quadruple(num):
...     # prints INFO:root: num == 5 to the example.log file
...     logging.info(f" num == {num}")
...     return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))

assert

assert عبارة ينبغي أن تُقيَّم دائمًا إلى True ما لم يكن في البرنامج خطأ. وعندما تُقيَّم عبارة assert إلى False فإنها ترفع AssertionError. وقد يتضمن التتبّع الخاص بالخطأ AssertionError رسالة اختيارية تكون جزءًا من عبارة assert. ورغم أن الرسالة اختيارية، فمن الممارسات الجيدة أن تُدرج دائمًا في تعريف assert.

فيما يلي مثال على استخدام assert:

>>> def int_division(dividend, divisor):
...     assert divisor != 0, "divisor must not be 0"
...     return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, 0))
Traceback (most recent call last):
  File <stdin>, line 7, in <module>
    print(int_division(2, 0))
          ^^^^^^^^^^^^^^^^^^
  File <stdin>, line 2, in int_division
    assert divisor != 0, "divisor must not be 0"
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must not be 0

إذا بدأنا قراءة التتبّع من الأسفل (كما ينبغي) فسنرى سريعًا أن المشكلة هي أنه لا ينبغي تمرير 0 باعتباره divisor.

ويمكن أيضًا استخدام assert للتحقق من أن قيمة ما من النوع المتوقع:

>>> import numbers
...
...
... def int_division(dividend, divisor):
...     assert divisor != 0, "divisor must not be 0"
...     assert isinstance(divisor, numbers.Number), "divisor must be a number"
...     return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, '0'))
Traceback (most recent call last):
  File <stdin>, line 11, in <module>
    print(int_division(2, '0'))
          ^^^^^^^^^^^^^^^^^^^^
  File <stdin>, line 6, in int_division
    assert isinstance(divisor, numbers.Number), "divisor must be a number"
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must be a number

بمجرد تحديد الخطأ، فكّر في استبدال assert بمعالجة الأخطاء. وذلك لأن جميع عبارات assert يمكن تعطيلها بتشغيل Python مع الخيارين -O أو -OO، أو بضبط متغير البيئة PYTHONOPTIMIZE على 1 أو 2. ضبط PYTHONOPTIMIZE على 1 يكافئ تشغيل Python مع الخيار -O، الذي يعطّل التأكيدات. وضبط PYTHONOPTIMIZE على 2 يكافئ تشغيل Python مع الخيار -OO، الذي يعطّل التأكيدات ويزيل سلاسل التوثيق من الكود البايتي. وتقليل الكود البايتي إحدى طرق جعل الكود يعمل أسرع.

منقّح الأخطاء في Python

يمتلك Python منقّح أخطاء مدمجًا، هو pdb. ويمكن استخدامه لتنفيذ الكود خطوة بخطوة وفحص المتغيرات. ويمكنك أيضًا ضبط نقاط توقف به. للبدء، عليك أولًا أن تنفّذ import pdb ثم تستدعي pdb.set_trace() حيث تريد بدء التنقيح:

import pdb

def add(num1, num2):
    return num1 + num2

pdb.set_trace()
sum = add(1,5)
print(sum)

سيؤدي تشغيل هذا الكود إلى إعطائك موجه pdb يمكنك كتابة الأوامر فيه. اكتب help للحصول على قائمة بالأوامر. أكثرها شيوعًا هو step الذي يدخل إلى دالة مُستدعاة في ذلك السطر. وnext يتخطى نداء دالة وينتقل إلى السطر التالي. وwhere يخبرك بالسطر الذي أنت فيه. ومن الأوامر المفيدة الأخرى whatis <variable> الذي يخبرك بنوع متغير، وprint(<variable>) الذي يطبع قيمة متغير. ويمكنك أيضًا استخدام <variable> فقط لطباعة قيمة متغير. وثمة أمر آخر هو jump <line number> الذي ينتقل إلى رقم سطر محدد.

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

>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> step
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)
>>> whatis num1
... <class 'int'>
>>> print(num2)
... 5
>>> next
... > c:\pdb.py(4)add()
... -> return num1 + num2
... (Pdb)
>>> jump 3
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)

تُضبط نقاط التوقف عبر break <filename>:<line number> <condition> حيث الشرط شرط اختياري يجب أن يتحقق حتى تُفعَّل نقطة التوقف. ويمكنك ببساطة كتابة break للحصول على قائمة بنقاط التوقف التي ضبطتها. ولتعطيل نقطة توقف يمكنك كتابة disable <breakpoint number>. ولتمكين نقطة توقف يمكنك كتابة enable <breakpoint number>. ولحذف نقطة توقف يمكنك كتابة clear <breakpoint number>. ولمواصلة التنفيذ يمكنك كتابة continue أو c. وللخروج من المنقّح يمكنك كتابة quit أو q.

فيما يلي مثال على كيفية استخدام أوامر المنقّح أعلاه استنادًا إلى الكود السابق:

>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> break
...
>>> break pdb:4
... Breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type         Disp Enb   Where
... 2   breakpoint   keep yes   at c:\pdb.py:4
>>> c # continue
... > c:\pdn.py(4)add()
... -> return num1 + num2
>>> disable break 1
... Disabled breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type         Disp Enb   Where
... 1   breakpoint   keep no    at c:\pdb.py:4
...         breakpoint already hit 1 time
>>> clear break 1
... Deleted breakpoint 1 at c:\pdb.py:4
>>> break
...

في Python 3.7+ ثمة طريقة أسهل لإنشاء نقاط التوقف. مجرد كتابة breakpoint() حيث تحتاجها ستنشئ نقطة توقف.

def add(num1, num2):
    breakpoint()
    return num1 + num2

breakpoint()
sum = add(1,5)
print(sum)
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> c # continue
... > c:\pdb.py(5)add()
... -> return num1 + num2