8. Hibák és kivételek

Eddig csak említettük a hibajelzéseket, de ha kipróbáltad a példákat feltehetően láttál néhányat. Kétfajta megkülönböztetendő hibafajta van: szintaktikai hiba (syntax error) és kivétel (exception).

8.1. Szintaktikai hibák

A szintaktikai hibák, vagy másképp elemzési hibák, talán a leggyakoribb hibaüzenetek, amíg tanulod a Pythont:

>>> while 1 print('Hello világ')
  File "<stdin>", line 1
    while 1 print('Hello világ')
                ^
SyntaxError: invalid syntax

Az elemző megismétli a hibás sort, és kitesz egy kis ,,nyilat” amely a sorban előforduló legelsőnek észlelt hibára mutat. A hibát a nyilat megelőző szócska (token) okozza (vagy legalábbis itt észlelte az értelemező): a példában, a hibát a print utasításnál észlelte, mivel a kettőspont (':') hiányzik előle. A fájl neve és a sorszám kiíródik, így tudhatod, hol keressed, ha egy szkriptet futtattál.

8.2. Kivételek

Ha egy állítás vagy kifejezés szintaktikailag helyes, akkor is okozhat hibát, ha megpróbáljuk végrehajtani. A végrehajtás során észlelt hibákat kivételeknek (execptions) nevezzük és nem feltétlenül végzetesek: nemsokára megtanulod, hogyan kezeld ezeket a Python programban. A legtöbb kivételt általában nem kezelik a programok, ekkor az alábbiakhoz hasonló hibaüzeneteket adnak:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

A hibaüzenetek utolsó sora mutatja, mi történt. Különböző típusú kivételeket kaphatunk, és a típus az üzenet részeként kerül kiíratásra: a típusok a példákban: ZeroDivisionError (nullával való osztás), NameError (név hiba) és TypeError (típussal kapcsolatos hiba). A kivétel típusaként kiírt szöveg a fellépő kivétel beépített (built-in) neve. Ez minden belső kivételre igaz, de nem feltétlenül igaz a felhasználó által definiált kivételekre (habár ez egy hasznos megállapodás). A szokásos kivételnevek belső azonosítók (nem fenntartott kulcsszavak).

A sor fennmaradó része egy részletezés, amelynek alakja a kivétel fajtájától függ.

Az ezt megelőző része a hibaüzenetnek megmutatja a szövegkörnyezetet – ahol a kivétel történt – egy veremvisszakövetés (stack backtrace) formájában. Általában ez veremvisszakövetést tartalamazza egy listában a forrás sorait. jóllehet, ez nem fog megjeleníteni a sztenderd bementeről kapott sorokat.

A Python Library Reference felsorolja a belső kivételeket és a jelentéseiket.

8.3. Kivételek kezelése

Van rá lehetőség, hogy olyan programokat írjunk, amik kezelik a különböző kivételeket (exception). Nézzük csak a következő példát, amely addig kérdezi a felhasználótól az értéket amíg egy egész számot nem ír be, de megengedi a felhasználónak, hogy megszakítsa a program futását (a Control-C használatával, vagy amit az operációs rendszer támogat). Jegyezzük meg, hogy a felhasználó által létrehozott megszakítás KeyboardInterrupt kivételként lép fel.

>>> while 1:
...     try:
...         x = int(raw_input("Írj be egy számot: "))
...         break
...     except ValueError:
...         print("Ez nem egész szám. Próbáld újra... ")
...

A try utasítás a következőképpen működik.

  • Először a try-mellékág hajtódik végre, azaz a try és except közötti utasítások.
  • Ha nem lép fel kivétel, az except-ágat a program átugorja, és a try utasítás befejeződik.
  • Ha kivétel lép fel a try-ág végrehajtása során, az ág maradék része nem hajtódik végre. Ekkor – ha a típusa megegyezik az except kulcsszó után megnevezett kivétellel, ez az expect-ág hajtódik végre, és ezután a végrehajtás a try–except blokk után folytatódik.
  • Ha olyan kivétel lép fel, amely nem egyezik a az expect-ágban megnevezett utasítással, akkor a kivételt átadja egy külsőbb try utasításnak. Ha nincs kezelve a kivétel, akkor ez egy kezeletlen kivétel a futás megáll egy utasítással, ahogy korábban láttuk.

A try utasításnak lehet egynél több except-ága is, hogy a különböző kivételeket kezelhessük. Egynél több kezelő hajtódhat végre. A kezelők csak a hozzájuk tartozó try-ágban fellépő kivételt kezelik a try utasítás másik kezelőjében fellépő kivételt nem. Egy expect-ágat több névvel is illethetünk egy kerek zárójelbe tett lista segítségével, például:

... except (RuntimeError, TypeError, NameError):
...     pass

(Magyarul: Futásidejű hiba, TípusHiba, NévHiba)

Egy osztá Az except-ágban egy osztály egyenértékű magával az osztállyal, vagy annak egy szülőosztályával (de a másik irány nem igaz — egy except-ág, amely a származtatott osztályt tartalmazza nem egyenértékű a szülőösztállyal). Például a következő kód B, C, D betűket fogja kiírni ebben a sorrendben:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

Vegyük észre, hogyha az except-ágak fordított sorrendben szerepelnének (az except B lenne az első), B, B, B betűket írt volna ki — az első illeszkedő ág hajtódik végre.

Az utolsó expect-ág esetén elhagyható a kivétel neve. Rendkívül óvatosan használjuk, mert elfedhet valódi programozási hibákat! Arra is használható, hogy kiírjunk egy hibaüzenetet, majd újra kivételdobást hajtsunk végre, (megengedve a hívónak, hogy lekezelje a kivételt):

import string, sys

try:
    f = open('file.txt')
    s = f.readline()
    i = int(string.strip(s))
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Nem képes az adatot egész számmá alakítani.")
except:
    print("Váratlan hiba:", sys.exc_info()[0])
    raise

A try ... except utasításnak lehet egy else-ága is, amelyet – ha létezik – az összes except-ágnak meg kell előznie. Ez nagyon hasznos olyan kódokban, amelyeknek mindenképpen végre kell hajtódniuk, ha a try ágban nem lép fel kivétel. Például:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('nem nyitható meg', arg)
    else:
        print(arg, len(f.readlines()), 'sorból áll.')
        f.close()

Az else-ág használata jobb, mintha a kódot a try-ághoz adnánk, mivel ez elkerüli egy olyan kivétel elkapását, amely nem a try ... except utasítások által védett ágban vannak.

Ha kivétel lép fel, lehetnek hozzátartozó értékei, amelyeket a kivétel argumentumának is nevezünk. A megléte és a típusa a kivétel fajtájától is függ. Azokra a kivételtípusokra, amelyek argumentummal rendelkeznek, az except ág előírhat egy változót a kivétel neve (vagy a lista) után, amely felveszi az argumentum értékét, ahogy itt látható:

>>> try:
...     spam()
... except NameError, x:
...     print('A(z)', x, 'név nincs definiálva.')
...
A(z) spam név nincs definiálva.

Ha a kivételnek argumentuma van, az mindíg utolsó részeként kerül a képernyőre.

A kivételkezelők nem csak akkor kezelik a kivételeket, ha azok ténylegesen a try ágban szerepelnek, hanem akkor is, ha azok valamelyik try-ágban meghívott függvényben szerepelnek (akár közvetve is). Például:

>>> def ez_rossz():
...     x = 1/0
...
>>> try:
...     ez_rossz()
... except ZeroDivisionError, detail:
...     print('Handling run-time error:', detail)
...
Handling run-time error: integer division or modulo

8.4. Kivételek létrehozása

A raise utasítás lehetővé teszi a programozó számára, hogy egy új, általa megadott kivételt hozzon létre. Például:

>>> raise NameError('IttVagyok')
Traceback (most recent call last):
  File "<stdin>", line 1
NameError: IttVagyok

A raise első argumentuma a kivétel neve amit létrehozunk. Az esetleges második argumentum adja meg a kivétel argumentumát.

8.5. Felhasználó által létrehozott kivételek

A programok elnevezhetik a saját kivételeiket, ha karakterláncot rendelnek egy változóhoz, vagy egy új kivétel-osztályt hoznak létre. Például:

>>> class MyError:
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return `self.value`
...
>>> try:
...     raise MyError(2*2)
... except MyError, e:
...     print('A kivételem fellépett, értéke:', e.value)
...
A kivételem fellépett, értéke: 4
>>> raise MyError, 1
Traceback (most recent call last):
  File "<stdin>", line 1
__main__.MyError: 1

Sok – a Pythonban megtalálható – modul ezt használja a függvényekben előforduló esetleges hibák megjelenítésére.

Bővebb információ az osztályokról a Osztályok fejezetben található.

8.6. Tisztogató eljárások definiálása

A try utasításnak van egy másik opcionális ága, mely tisztogató műveletek létrehozására szolgál, amelyeket mindenképpen végre kell hajtani. Például:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Viszlát világ!')
...
Viszlát világ!
KeyboardInterrupt

A finally-ág mindenképpen végrehajtódik, mielőtt elhagyjuk a try utasítást, akár történt kivétel, akár nem. Ha kivételdobás történt a try-ágban, és egyik except-ág sem kezelte (vagy esetleg az except- vagy else-ágban lépett fel), a kivétel a finally-ág végrehajtása után újra kiváltódik. A finally-ág akkor is végrehajtódik “kifelé úton”, amikor a try utasítást elhagyjuk egy break, egy continue vagy egy return utasítás következtében. Egy összetettebb példa:

>>> def oszt(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("nullával osztás!")
...     else:
...         print("az eredmény", result)
...     finally:
...         print("a finally ág végrehajtása")
...
>>> oszt(2, 1)
az eredmény 2.0
a finally ág végrehajtása
>>> oszt(2, 0)
nullával osztás!
a finally ág végrehajtása
>>> oszt("2", "1")
a finally ág végrehajtása
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Ahogy látjuk a finally-ág minden esetben végrehajtódott. A TypeError dobását, amelyet a két karakterlánc osztása okozott, nem kezelte egyik except-ág sem, ezért újra lett dobva, miután a finally ág lefutott.

A valódi alkalmazások esetén a finally-ág akkor hasznos, ha külső erőforrásokat kell eleresztenünk (amilyenek a fájlok, vagy hálózati kapcsolatok), függetlenül attól, hogy az erőforrás felhasználása sikeres volt-e.

8.7. Előre definiált tisztogató eljárások

Pár objektum sztenderd tisztogató eljárásokat definiál arra az esetre, amikor az objektum szükségtelenné válik, tekintet nélkül arra, hogy az objektummal végzett dolgok sikeresen vagy sikertelenül zárultak. Nézzük a következő példát, amely meg próbál nyitni egy fájlt, és kiírni a tartalmát a képernyőre.

for sor in open("én_fájlom.txt"):
    print(sor, end="")

Az a baj ezzel a kóddal, hogy a fájlt nyitva hagyja miután ez a kód végrehajtásra került. Ez nem jelent gondot egyszerűbb szkriptekben, de nagyobb alkalmazások esetén gondot okozhat. A with utasítás lehetővé teszi olyan objektumok esetén, mint a fájlok, hogy olyan módon hasznájuk, amely lehetővé teszi, hogy mindig azonnal és megfelelő módon tisztogassunk.

with open("én_fájlom.txt") as f:
    for sor in f:
        print(sor, end="")

Miután az utasítás végrehajtódik, az f fájl mindig zárva lesz, akkor is, ha a sorok feldolgozása közben baj történt. Az olyan objektumok, mint amilyenek a fájlok, előre definiált tisztogató eljárásokkal rendelkeznek. Ezt a dokumentációjukból megtudhatjuk.