چطور tracebackها را بخوانیم

مروری بر نحوه‌ی خواندن tracebackهای Python برای debug کردن.


شیء فریم

وقتی تابعی فراخوانی می‌شود، یک شیء فریم ساخته می‌شود تا متغیرهای محلی و آرگومان‌های داده‌شده به تابع را در خود نگه دارد. وقتی تابع بازمی‌گردد، شیء فریم از بین می‌رود. وقتی تابع B درون تابع A فراخوانی می‌شود، مقادیر تابع B در یک شیء فریم قرار می‌گیرد و سپس آن شیء فریم روی شیء فریم تابع A در پشته‌ی فراخوانی قرار می‌گیرد.

پشته‌ی فراخوانی

پشته‌ی فراخوانی مجموعه‌ای از شیءهای فریم برای توابع فعال در همان لحظه است. اگر تابع A تابع B را فراخوانی کرده باشد و تابع B تابع C را فراخوانی کرده باشد، شیءهای فریم هر سه تابع روی پشته‌ی فراخوانی خواهند بود. به محض اینکه تابع C بازگردد، شیء فریم آن از پشته بیرون می‌آید و تنها شیءهای فریم توابع A و B روی پشته‌ی فراخوانی باقی می‌مانند.

ردیابی

ردیابی گزارشی از همه‌ی شیءهای فریم روی پشته در یک زمان مشخص است. وقتی یک برنامه‌ی پایتون به یک استثنای مدیریت‌نشده برمی‌خورد، پیام استثنا و یک ردیابی را چاپ می‌کند. ردیابی نشان می‌دهد استثنا کجا ایجاد شده و چه توابعی تا رسیدن به آن نقطه فراخوانی شده‌اند.

نحوه‌ی خواندن یک ردیابی

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)

با حرکت از پایین به عقب می‌بینیم فراخوانی‌ای که استثنا در آن رخ داده، در خط ۲ درون my_func است. و با فراخوانی my_func در خط ۵ به آنجا رسیده‌ایم.

استثناهای رایج

پایتون بیش از ۶۰ کلاس استثنای توکار تعریف می‌کند. در ادامه مروری کوتاه بر برخی از رایج‌ترین استثناها و اینکه هرکدام به چه چیزی اشاره دارند می‌آید.

SyntaxError

پایتون وقتی به دلیل نحوه‌ی نگارش نامعتبر نتواند کد را بفهمد، 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

پایتون وقتی یک دستور assert (پایین‌تر را ببینید) شکست بخورد، AssertionError ایجاد می‌کند.

برای مشاهده‌ی یک نمونه کد اینجا کلیک کنید.

اجرای این کد:

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 وقتی ایجاد می‌شود که کد بخواهد چیزی را import کند، اما پایتون نتواند این کار را انجام دهد. برای نمونه، یک تست واحد برای 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 مقدار معتبری برای جذر نیست. به زبان فنی (ریاضی)، منفی ۱ در دامنه‌ی جذرها قرار ندارد.

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 را می‌توان با اجرای پایتون همراه با گزینه‌های -O یا -OO، یا با تنظیم متغیر محیطی PYTHONOPTIMIZE روی 1 یا 2 غیرفعال کرد. تنظیم PYTHONOPTIMIZE روی 1 معادل اجرای پایتون با گزینه‌ی -O است که assertها را غیرفعال می‌کند. تنظیم PYTHONOPTIMIZE روی 2 معادل اجرای پایتون با گزینه‌ی -OO است که هم assertها را غیرفعال می‌کند و هم docstringها را از بایت‌کد حذف می‌کند. کاهش حجم بایت‌کد یکی از راه‌های سریع‌تر اجرا شدن کد است.

دیباگر پایتون

پایتون یک دیباگر توکار دارد، 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
...

در پایتون ۳.۷ و بالاتر، راه ساده‌تری برای ایجاد نقاط توقف وجود دارد. کافی است هرجا لازم بود 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