ट्रेसबैक कैसे पढ़ें

डीबगिंग के लिए Python ट्रेसबैक पढ़ने का एक अवलोकन।


फ्रेम ऑब्जेक्ट

जब किसी फंक्शन को कॉल किया जाता है, तो एक फ्रेम ऑब्जेक्ट बनाया जाता है, जिसमें उस फंक्शन के लोकल वेरिएबल और उसे दिए गए आर्गुमेंट रखे जाते हैं। जब फंक्शन अपना काम पूरा कर लेता है, तो फ्रेम ऑब्जेक्ट हटा दिया जाता है। जब फंक्शन A के अंदर फंक्शन B को कॉल किया जाता है, तो फंक्शन 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)

नीचे से ऊपर की ओर चलते हुए हम देखते हैं कि जहाँ एक्सेप्शन हुआ वह कॉल my_func की लाइन 2 पर है। हम वहाँ लाइन 5 पर my_func को कॉल करके पहुँचे।

आम एक्सेप्शन

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

जब कोई assert स्टेटमेंट (नीचे देखिए) विफल होता है, तो Python 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".lower() होने पर "Hello".lowercase() लिख देना। "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

अगर हम ट्रेसबैक को नीचे से पढ़ना शुरू करें (जैसा कि हमें करना चाहिए), तो हमें तुरंत पता चलता है कि divisor के रूप में 0 नहीं दिया जाना चाहिए था।

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> से सेट किए जाते हैं, जहाँ 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