Hogyan olvasd a tracebackeket

Áttekintés arról, hogyan olvasd a Python tracebackeket a hibakereséshez.


Frame objektum

Amikor egy függvényt meghívunk, létrejön egy frame objektum, amely a függvény lokális változóit és a neki átadott argumentumokat tárolja. Amikor a függvény visszatér, a frame objektum megszűnik. Amikor az A függvényen belül meghívjuk a B függvényt, a B függvény értékei egy frame objektumba kerülnek, amely aztán a hívási verem tetején az A függvény frame objektuma fölé kerül.

Hívási verem

A hívási verem az éppen aktív függvények frame objektumainak gyűjteménye. Ha az A függvény meghívta a B függvényt, és a B függvény meghívta a C függvényt, akkor mindhárom függvény frame objektuma a hívási vermen lesz. Amint a C függvény visszatér, a frame objektuma lekerül a veremről, és csak az A és B függvény frame objektuma marad a hívási vermen.

Traceback

A traceback az adott pillanatban a vermen lévő összes frame objektumról készült jelentés. Amikor egy Python-program kezeletlen kivételbe fut, kiírja a kivétel üzenetét és egy tracebacket. A traceback megmutatja, hol keletkezett a kivétel, és mely függvények hívásai vezettek oda.

Hogyan olvassunk egy tracebacket

A ValueError gyakori kivétel.

Az alábbi példa egy ValueError-ra, amely akkor keletkezik, amikor a bal oldalon két változónak próbálunk értéket adni a jobb oldalon lévő egyetlen értékből:

>>> 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)

A tracebackeknél a legutóbbi hívás kerül a végére, ezért a traceback olvasását a legalul lévő kivétellel érdemes kezdeni. Onnan felfelé haladva láthatjuk, hogyan jutottunk el ahhoz az utasításhoz. Ha a hibás sort egy függvénybe tesszük, majd meghívjuk a függvényt, hosszabb tracebacket látunk:

>>> 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)

Alulról visszafelé haladva láthatjuk, hogy a hívás, ahol a kivétel keletkezett, a 2. sorban van, a my_func függvényben. Oda úgy jutottunk el, hogy az 5. sorban meghívtuk a my_func függvényt.

Gyakori kivételek

A Python több mint 60 beépített kivételosztályt definiál. Íme egy rövid áttekintés néhány gyakoribb kivételről és arról, hogy mire utalnak.

SyntaxError

A Python SyntaxError-t dob, amikor érvénytelen szintaxis miatt nem tudja megérteni a kódot. Előfordulhat például, hogy van egy nyitó zárójel, de hiányzik a hozzá tartozó csukó zárójel.

Kattints ide egy kódpéldáért.

A következő kód futtatása:

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

az ehhez hasonló stack trace-t eredményezi (figyeld meg az utolsó sorban lévő üzenetet):

.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

A Python AssertionError-t dob, amikor egy assert utasítás (lásd alább) meghiúsul.

Kattints ide egy kódpéldáért.

A következő kód futtatása:

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


distance("ab", "abc")

az ehhez hasonló stack trace-t eredményezi (figyeld meg az utolsó sorban lévő üzenetet):

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 akkor keletkezik, amikor a kód (vagy egy unit teszt!) egy objektum attribútumához próbál hozzáférni, de az objektumnak nincs ilyen attribútuma. Például egy unit teszt azt várja, hogy egy Robot objektumnak legyen direction attribútuma, de amikor megpróbál hozzáférni a robot.direction-höz, az nem létezik.

Ez elírásra is utalhat, például ha "Hello".lowercase()-t használunk, amikor a helyes szintaxis a "Hello".lower(). A "Hello".lowercase() a következőt dobja: AttributeError: 'str' object has no attribute 'lowercase'.

Kattints ide egy példáért a Robot osztályra.

A következő kód futtatása:

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

az ehhez hasonló stack trace-t eredményezi (figyeld meg az utolsó sorban lévő üzenetet):

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'
Kattints ide egy stringpéldáért.

A következő kód futtatása:

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


distance("ab", "abc")

az ehhez hasonló stack trace-t eredményezi (figyeld meg az utolsó sorban lévő üzenetet):

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

ImportError

ImportError akkor keletkezik, amikor a kód megpróbál importálni valamit, de a Python erre nem képes. Például a Guidos Gorgeous Lasagna egyik unit tesztje a from lasagna import bake_time_remaining utasítást hajtja végre, de a lasgana.py megoldásfájl esetleg nem definiálja a bake_time_remaining függvényt.

Kattints ide egy kódpéldáért

A lasgana.py fájl futtatása a definiált függvény nélkül a következő hibát eredményezné:

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.

A kód futtatása az alábbihoz hasonló hibát eredményez. (Figyeld meg az utolsó sort.)

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

Az IndexError-hoz hasonlóan ez a kivétel akkor keletkezik, amikor egy kulccsal próbálunk kikeresni egy szótárértéket, de a kulcs nem szerepel a szótárban.

Kattints ide egy kódpéldáért

Tekintsd a következő kódot.

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.

A kód futtatása az alábbihoz hasonló hibát eredményez. (Figyeld meg az utolsó sort.)

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

A TypeError általában akkor keletkezik, amikor hibás adattípust adunk át egy függvénynek, vagy használunk egy műveletben.

Kattints ide egy kódpéldáért

Tekintsd a következő kódot.

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


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

A kód futtatása az alábbihoz hasonló hibát eredményez. (Figyeld meg az utolsó sort.)

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

A ValueError általában akkor keletkezik, amikor érvénytelen értéket adunk át egy függvénynek.

Kattints ide egy kódpéldáért

Megjegyzés: valós négyzetgyök csak pozitív számoknak létezik. A math.sqrt(-1) hívása ValueError: math domain error kivételt dob, mert a -1 nem érvényes érték négyzetgyökhöz. (Matematikai) szaknyelven a -1 nincs benne a négyzetgyök értelmezési tartományában.

import math

math.sqrt(-1)

A kód futtatása az alábbihoz hasonló hibát eredményez. (Figyeld meg az utolsó sort.)

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

A print függvény használata

Néha nem keletkezik hiba, de egy érték mégsem az, amit várnánk. Ez különösen zavaró lehet, ha az érték számítások láncolatának eredménye. Ilyen esetben segíthet, ha minden lépésnél megnézzük az értéket, hogy lássuk, melyik lépés az, amelyik nem a várt módon viselkedik. A print függvénnyel kiírhatjuk az értéket a konzolra. Az alábbi példa egy olyan függvényre, amely nem a várt értéket adja vissza:

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

Amikor a függvénynek 5-öt adunk át, a várt érték 8, de 10.0-t ad vissza. A hibakereséshez a számítást részekre bontjuk, hogy minden lépésnél megvizsgálhassuk az értéket.

# 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))

A szint INFO-ra van állítva, mert az alapértelmezett szint a WARNING. Tartós naplózáshoz beállíthatjuk, hogy a naplózó fájlba írjon, így:

>>> 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

Az assert olyan utasítás, amelynek mindig True-ra kell kiértékelődnie, kivéve, ha hiba van a programban. Amikor egy assert False-ra értékelődik ki, AssertionError kivételt dob. Az AssertionError tracebackje tartalmazhat egy opcionális üzenetet, amely az assert utasítás része. Bár az üzenet elhagyható, jó gyakorlat mindig belefoglalni egyet az assert definíciójába.

Az alábbi példa az assert használatára:

>>> 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

Ha a tracebacket alulról kezdjük olvasni (ahogy kell), gyorsan láthatjuk, hogy a probléma az, hogy a 0-t nem lett volna szabad divisor-ként átadni.

Az assert arra is használható, hogy ellenőrizzük, egy érték a várt típusú-e:

>>> 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

Amint azonosítottuk a hibát, fontold meg, hogy az assert-et hibakezeléssel helyettesíted. Ennek az az oka, hogy minden assert utasítás letiltható, ha a Pythont a -O vagy -OO kapcsolóval futtatjuk, vagy ha a PYTHONOPTIMIZE környezeti változót 1-re vagy 2-re állítjuk. A PYTHONOPTIMIZE 1-re állítása megegyezik azzal, hogy a Pythont a -O kapcsolóval futtatjuk, ami letiltja az assert utasításokat. A PYTHONOPTIMIZE 2-re állítása megegyezik azzal, hogy a Pythont a -OO kapcsolóval futtatjuk, ami egyszerre tiltja le az assert utasításokat és távolítja el a docstringeket a bytecode-ból. A bytecode csökkentése az egyik módja annak, hogy a kód gyorsabban fusson.

Python debugger

A Pythonnak van egy beépített debuggere, a pdb. Ezzel lépésenként végighaladhatunk a kódon, és megvizsgálhatjuk a változókat. Töréspontokat is beállíthatsz vele. A kezdéshez először import pdb-t kell írnod, majd meg kell hívnod a pdb.set_trace()-t ott, ahol a hibakeresést indítani szeretnéd:

import pdb

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

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

A kód futtatásakor egy pdb prompt jelenik meg, ahová beírhatod a parancsokat. Írd be a help parancsot a parancsok listájáért. A leggyakoribbak közül a step belép a soron meghívott függvénybe. A next átlép a függvényhíváson, és a következő sorra lép. A where megmutatja, melyik sorban jársz. Néhány más hasznos parancs: a whatis <variable>, ami megmondja egy változó típusát, és a print(<variable>), ami kiírja egy változó értékét. Egy változó értékét egyszerűen a <variable> beírásával is kiírhatod. Egy másik parancs a jump <line number>, ami egy adott sorszámra ugrik.

Íme egy kis példa a debugger használatára a korábbi kód alapján. Vedd figyelembe, hogy ennél és a következő példáknál a MacOS vagy Linux platformokon a fájlelérési utak perjelet használnak:

>>> 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)

A töréspontokat a break <filename>:<line number> <condition> paranccsal állíthatod be, ahol a feltétel egy opcionális feltétel, amelynek igaznak kell lennie ahhoz, hogy a töréspont aktiválódjon. Egyszerűen beírhatod a break parancsot, hogy listát kapj a beállított töréspontokról. Egy töréspont letiltásához a disable <breakpoint number> parancsot írhatod be. Egy töréspont engedélyezéséhez az enable <breakpoint number> parancsot írhatod be. Egy töréspont törléséhez a clear <breakpoint number> parancsot írhatod be. A végrehajtás folytatásához a continue vagy c parancsot írhatod be. A debuggerből való kilépéshez a quit vagy q parancsot írhatod be.

Íme egy példa a fenti debugger-parancsok használatára a korábbi kód alapján:

>>> 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
...

A Python 3.7+ verzióiban egyszerűbb módja is van a töréspontok létrehozásának. Ha egyszerűen beírod a breakpoint()-ot oda, ahol szükség van rá, létrejön egy.

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