Advertisement
  1. Code
  2. Python

Profi hibakezelés Pythonban

Scroll to top
Read Time: 11 min

() translation by (you can also view the original English article)

Ebből a tutorialból megtudhatod, hogyan kezelhetsz hibás helyzeteket Pythonban, a teljes rendszer szemszögéből. A hibakezelés a tervezés egy kritikus pontja, és egészen az alacsony szintű megvalósításoktól (néha hardver szinten) a végfelhasználókig szükség van rá. Ha nincs egy következetes időszerű stratégiád, a rendszered megbízhatatlan lesz, a felhasználói élmény gyenge, és sok debuggolással és hibakereséssel kell megbírkóznod.

A siker kulcsa, hogy tisztában legyünk ezekkel a kapcsolódó aspektusokkal, világosan mindet végiggondolva és olyan megoldást adjunk, ami érint minden kérdéses területet.

Státusz kódok kontra Kivételek

Két fő hibakezelő modell van: a státusz kódok és a kivétel kezelés. Státusz kódokat bármilyen programozási nyelvben használhatunk. A kivételek használatához a nyelv támogatása szükséges.

Python nyelv támogatja a kivételeket. A Python és a standard könyvtára bőségesen el van látva kivételekkel pl. IO hibákat jelez, nullával való osztást, indexelés tartomány túllépést és néhány nem jellemző esetben is, mint pl. az iteráció vége esetén (bár ez egy rejtett dolog). A legtöbb függvénykönyvtár így működik és kivételeket dob.

Ez azt jelenti, hogy a kódodnak le kell tudnia kezelni a kivételeket, amiket a Python és könyvtárai dobnak, és ezen felül te is dobhatsz kivételeket a saját kódodban, amikor szükséges, és nem kell a státusz kódoktól függnöd.

Gyors példa

Mielőtt beleássuk magunkat a Python kivételkezelés és hibakezelés praktikáinak mélységeibe, lássunk néhány kivételkezelést a gyakorlatban:

1
def f():
2
3
    return 4 / 0
4
5
6
7
def g():
8
9
    raise Exception("Don't call us. We'll call you")
10
11
12
13
def h():
14
15
    try:
16
17
        f()
18
19
    except Exception as e:
20
21
        print(e)
22
23
    try:
24
25
        g()
26
27
    except Exception as e:
28
29
        print(e)

Ez a h() függvény kimenete:

1
h()
2
3
division by zero
4
5
Don't call us. We'll call you

Python kivételek

A Python kivételek osztály hierarchiába rendezett objektumok.

Itt van a teljes hierarchia:

1
BaseException
2
3
 +-- SystemExit
4
5
 +-- KeyboardInterrupt
6
7
 +-- GeneratorExit
8
9
 +-- Exception
10
11
      +-- StopIteration
12
13
      +-- StandardError
14
15
      |    +-- BufferError
16
17
      |    +-- ArithmeticError
18
19
      |    |    +-- FloatingPointError
20
21
      |    |    +-- OverflowError
22
23
      |    |    +-- ZeroDivisionError
24
25
      |    +-- AssertionError
26
27
      |    +-- AttributeError
28
29
      |    +-- EnvironmentError
30
31
      |    |    +-- IOError
32
33
      |    |    +-- OSError
34
35
      |    |         +-- WindowsError (Windows)
36
37
      |    |         +-- VMSError (VMS)
38
39
      |    +-- EOFError
40
41
      |    +-- ImportError
42
43
      |    +-- LookupError
44
45
      |    |    +-- IndexError
46
47
      |    |    +-- KeyError
48
49
      |    +-- MemoryError
50
51
      |    +-- NameError
52
53
      |    |    +-- UnboundLocalError
54
55
      |    +-- ReferenceError
56
57
      |    +-- RuntimeError
58
59
      |    |    +-- NotImplementedError
60
61
      |    +-- SyntaxError
62
63
      |    |    +-- IndentationError
64
65
      |    |         +-- TabError
66
67
      |    +-- SystemError
68
69
      |    +-- TypeError
70
71
      |    +-- ValueError
72
73
      |         +-- UnicodeError
74
75
      |              +-- UnicodeDecodeError
76
77
      |              +-- UnicodeEncodeError
78
79
      |              +-- UnicodeTranslateError
80
81
      +-- Warning
82
83
           +-- DeprecationWarning
84
85
           +-- PendingDeprecationWarning
86
87
           +-- RuntimeWarning
88
89
           +-- SyntaxWarning
90
91
           +-- UserWarning
92
93
           +-- FutureWarning
94
95
  +-- ImportWarning
96
97
  +-- UnicodeWarning
98
99
  +-- BytesWarning
100
 

Van néhány speciális kivétel, amelyek közvetlenül a BaseException osztályból származnak, mint pl. SystemExit, KeyboardInterrupt és GeneratorExit. Az Exception osztály az alap-ős osztálya a StopIterationStandardError és Warning osztályoknak. Az összes standard hiba a StandardError osztályból származik.

Amikor kivételt dobsz, vagy a függvény amit hívtál kivételt dob, a normál kód futtatása megáll, és a kivétel elkezd felfelé terjedni a hívásokon (call stack), egészen addig, amíg egy megfelelő kivétel kezelő el nem kapja. Ha nincs ilyen kivétel kezelő, ami elkapná, a folyamat (pontosabban az adott szál) befejeződik egy el nem kapott kivétel üzenettel.

Kivételek dobása

A kivételek dobása nagyon egyszerű. A raise kulcsszót kell használni, hogy egy olyan objektumot dobjunk, ami az Exception osztály gyermeke. Ez lehet konkrétan az Exception osztály maga, vagy az alap kivételek közül egy, pl. RuntimeError, vagy egy gyermek osztály, amit az Exception osztályból származtattál. Az alábbi kódrészlet mindegyik esetet bemutatja:

1
# Raise an instance of the Exception class itself

2
3
raise Exception('Ummm... something is wrong')
4
5
6
7
# Raise an instance of the RuntimeError class

8
9
raise RuntimeError('Ummm... something is wrong')
10
11
12
13
# Raise a custom subclass of Exception that keeps the timestamp the exception was created

14
15
from datetime import datetime
16
17
18
19
class SuperError(Exception):
20
21
    def __init__(self, message):
22
23
        Exception.__init__(message)
24
25
        self.when = datetime.now()
26
27
28
29
30
31
raise SuperError('Ummm... something is wrong')

Kivételek elkapása

Az except záradékban lehet kivételeket elkapni, ahogy a példában is láthattuk. Amikor elkapsz egy kivételt, három lehetőséged van:

  • Csöndben elhallgatod, elfeded (kezeled a hibát és tovább fut).
  • Csinálsz valamit, mint pl. logolás, de ha újradobod ugyanazt a kivételt, akkor magasabb szinteken lehet kezelni.
  • Másféle kivételt dobsz az eredeti helyett.

Kivétel elhallgatása

Ha tudod hogyan kell kezelni és teljesen helyreállítani egy kivétel okozta problémát, akkor el kellene fedni.

Például vegyük hogy bemenetként egy file tartalmat kapsz, ami különféle formátumú lehet (JSON, YAML), és megpróbálhatod parsolni különféle feldolgozókkal. Ha a JSON feldolgozó egy olyan kivételt dob hogy a file nem érvényes JSON file, akkor elfedheted és kipróbálhatod a YAML feldolgozót arra a file-ra. Ha a YAML feldolgozó sem jár sikerrel, akkor hagyjuk a kivételt felterjeszteni.

1
import json
2
3
import yaml
4
5
6
7
def parse_file(filename):
8
9
    try:
10
11
        return json.load(open(filename))
12
13
    except json.JSONDecodeError
14
15
        return yaml.load(open(filename))

Megjegyezzük,hogy más féle kivételek (pl. file nem található vagy nincs olvasási jogosultság) felterjesztődnek és nem fogja a specifikusan beállított kivétel záradék elkapni. Ez egy jó irányelv abban az esetben, amikor a YAML feldolgozót csak akkor akarod kipróbálni, ha a JSON feldolgozó nem járt sikerrel valamilyen JSON kódolási probléma miatt.

Ha minden kivételt kezelni szeretnél, akkor használd az except Exteption záradékot. Például:

1
def print_exception_type(func, *args, **kwargs):
2
3
    try:
4
5
        return func(*args, **kwargs)
6
7
    except Exception as e:
8
9
        print type(e)

Az as e kódot használva hozzákötöd a kivétel objektumot az e nevű változóhoz, ami a záradékban elérhető lesz.

Újra dobjuk ugyanazt a kivételt

Az újra dobáshoz csak adjuk hozzá a kivétel kezelésen belül a raise parancsot paraméterek nélkül. Ezzel helyileg tudod kezelni a problémát, de tovább dobva fentebbi szintek is értesülnek erről és kezelhetik. Itt az invoke_function() függvény kiírja a kivétel típusát a console-ra és újra dobja a kivételt.

1
def invoke_function(func, *args, **kwargs):
2
3
    try:
4
5
        return func(*args, **kwargs)
6
7
    except Exception as e:
8
9
        print type(e)
10
11
        raise

Különböző kivételek dobása

Van néhány eset, amikor másféle kivételeket is dobni szeretnél. Néha szeretnéd csoportosítani az alacsony szintű kivételeket egyetlen kategóriába, amit egységesen kezelnél egy magasabb szintű kódban. Más esetben, a kivételt a felhasználó szintjére kell vinni, és néhány alkalmazás specifikus információt is meg kell jeleníteni.

Finally blokk

Néha szeretnénk azt biztosítani, hogy egy tisztító kód lefusson akkor is, ha kivétel történt valamikor a program futása során. Például van egy adatbázis kapcsolatunk, amit szeretnék bezárni ha készen vagyunk. Ez egy rossz mód:

1
def fetch_some_data():
2
3
    db = open_db_connection()
4
5
    query(db)
6
7
    close_db_Connection(db)

Ha a query() függvény kivételd dob akkor a close_db_connection() sosem fog lefutni és a DB kapcsolat nyitva marad. A finally blokk mindig lefut miután az összes kivételkezelés is lefutott. Itt van, hogyan kell ezt jól csinálni:

1
def fetch_some_data():
2
3
    db = None
4
5
    try:
6
7
        db = open_db_connection()
8
9
        query(db)
10
11
    finally:
12
13
        if db is not None:
14
15
            close_db_connection(db)

Az open_db_connection() hívása nem biztos hogy egy kapcsolatot ad vissza, vagy mondjuk kivételt fog dobni. Ebben az esetben nincs szükség a DB kapcsolat lezárására.

Amikor a finally blokkot használjuk, óvatosnak kell lennünk, hogy ne dobjunk kivételt ezen belül, mert az eredeti kivételt elfedi.

Context manager protokoll

A context manager protokoll egy újabb mód arra, hogyan lehet az erőforrásokat mint pl. fileok, DB kapcsolatok, körülvenni a tiszta kódban, ami automatikusan végrehajtódik még ha kivételek is történtek. Try-finally blokkok helyett használhatjuk a with kifejezést. Itt van egy példa:

1
def process_file(filename):
2
3
     with open(filename) as f:
4
5
        process(f.read())

Ha a process() függvény kivételt dob, a file rendesen le lesz zárva azonnal, amikor a with blockból kilép a program, akár volt a kivétel kezelve akár nem.

Loggolás

A logolás eléggé alapvető követlemény a nem triviális sokáig futó rendszerekben. Különösen hasznos web alkalmazás esetén amikor a kivételeket általánosan kezelheted: Csak logold a kivételt és adj vissza egy hibaüzenetet a hívónak.

Amikor logolunk, hasznos a kivétel típusát, a hibaüzenetet és a híváslistát is logolni. Az összes információ elérhető a sys.exc_info objekumon keresztül, de használhatod a logger.exception() függvényt is. kivételkezelőnek, a Python logoló rendszere kiolvas minden fontos információt a számodra.

Ez a legjobb minta, amit javaslok:

1
import logging
2
3
logger = logging.getLogger()
4
5
6
7
def f():
8
9
    try:
10
11
        flaky_func()
12
13
    except Exception:
14
15
        logger.exception()
16
17
        raise

Ha követed ezt a mintát, akkor (feltéve ha a loggolást jól állítottad be) mindegy mi történik, elég jó bejegyzéseid lesznek a logokban arról, mi volt rossz, és ki fogod tudni javítani a feladatot.

Ha újra dobod a kivételt, biztosnak kell lenned, hogy nem logolod újra és újra ugyanazt a kivételt különbözö szinteken. Ez egy felesleges dolog, és össze is kavarhat, mert arra gondolhatsz hogy többször is előfordult az eset, de valójában egy problémát logolt többször a rendszer.

A legegyszerűbb mód az, ha hagyunk minden kivételt a végéig elmenni (kivéve ha biztosan lehet kezelni hamarabb) és a logolást az alkalmazás felső szintjének közelében végezzük el.

Őrszem

A logolás egy képesség. A legtöbb implementáció log fileokat használ. De a nagy teljesítményű elosztott rendszerek esetén, száz, ezer vagy több szerverrel felszerelve, nem biztos hogy ez a legjobb megoldás.

Ahhoz, hogy a kivételeket nyilvántartsd az egész infrastruktúrádban, egy olyan szolgáltatás, mint a sentry szuper hasznos. Központosítja a kivételek riportjait, és a hívások listáján kívül a környezet változóinak állapotát is tárolja, a változók értékeit a kivétel dobásakor. Nagyon szép felületet is nyújt vezérlőpulttal, riportokkal és az üzenetek szűrésével, akár több projekt esetén is. Nyílt kódú így saját szervert is futtathatsz vagy feliratkozhatsz a hostolt verzióra is.

Foglalkozzunk az átmeneti hibákkal

Néhány hiba csak átmeneti, különösen amikor elosztott rendszerekkel foglalkozunk. Egy rendszer, ami kiakad az első hibánál nem igazán hasznos.

Ha a kódod néhány távoli rendszert ér el, ami épp nem válaszol, a hagyományos megoldás a timeout, de néha nincs minden rendszerben timeout kezelés. Timeout-ot nem mindig könnyű kalibrálni ahogy a feltételek változnak.

Egy másik megközelítés az, hogy gyorsan ismerjük el a hibát, és próbáljuk újra. Ennek az az előnye, hogy ha a célgép gyorsan válaszol, akkor nem kell sok időt tölteni az altató feltétellel, és azonnal lehet reagálni. De ha hibára fut, többször újrapróbálhatod a kapcsolatot, amíg úgy nem döntesz, hogy valóban elérhetetlen, és dobsz egy kivételt. A következő részben bemutatok egy decorator-t, ami ezt megoldja számodra.

Hasznos díszítők

Két díszítő mintát mutatunk be, ami segít a hibakezelésben, az egyik a @log_error, ami egy kivételt logol majd újra dobja, a másik a @retry díszítő, ami megpróbálja néhányszor újra hívni a függvényt.

Hiba loggoló

Itt van egy egyszerű implementáció. A decorator egy loggoló objektumot vár. Amikor egy függvényt díszít és a függvényt meghívják, egy try-except blokkal veszi körül, és ha kivétel történik, logolni fogja és végül újradobja a kivételt.

1
def log_error(logger)
2
3
    def decorated(f):
4
5
        @functools.wraps(f)
6
7
        def wrapped(*args, **kwargs):
8
9
            try:
10
11
                return f(*args, **kwargs)
12
13
            except Exception as e:
14
15
                if logger:
16
17
                    logger.exception(e)
18
19
                raise
20
21
        return wrapped
22
23
    return decorated

Itt van, hogyan lehet használni:

1
import logging
2
3
logger = logging.getLogger()
4
5
6
7
@log_error(logger)
8
9
def f():
10
11
    raise Exception('I am exceptional')

Újra próbáló (retrier)

Egy nagyon jó implementációt láthatunk a @retry decorator-hoz.

1
import time
2
3
import math
4
5
6
7
# Retry decorator with exponential backoff

8
9
def retry(tries, delay=3, backoff=2):
10
11
  '''Retries a function or method until it returns True.

12


13


14


15
  delay sets the initial delay in seconds, and backoff sets the factor by which

16


17
  the delay should lengthen after each failure. backoff must be greater than 1,

18


19
  or else it isn't really a backoff. tries must be at least 0, and delay

20


21
  greater than 0.'''
22
23
24
25
  if backoff <= 1:
26
27
    raise ValueError("backoff must be greater than 1")
28
29
30
31
  tries = math.floor(tries)
32
33
  if tries < 0:
34
35
    raise ValueError("tries must be 0 or greater")
36
37
38
39
  if delay <= 0:
40
41
    raise ValueError("delay must be greater than 0")
42
43
44
45
  def deco_retry(f):
46
47
    def f_retry(*args, **kwargs):
48
49
      mtries, mdelay = tries, delay # make mutable

50
51
52
53
      rv = f(*args, **kwargs) # first attempt

54
55
      while mtries > 0:
56
57
        if rv is True: # Done on success

58
59
          return True
60
61
62
63
        mtries -= 1      # consume an attempt

64
65
        time.sleep(mdelay) # wait...

66
67
        mdelay *= backoff  # make future wait longer

68
69
70
71
        rv = f(*args, **kwargs) # Try again

72
73
74
75
      return False # Ran out of tries :-(

76
77
78
79
    return f_retry # true decorator -> decorated function

80
81
  return deco_retry  # @retry(arg[, ...]) -> true decorator

Konklúzió

A hibakezelés kritikus fontosságú mind a felhasználóknak mind a fejlesztőknek. A Python nyelv és az alapkönyvtár nagyon jó támogatást nyújt a kivétel alapú hibakezeléshez. Ha gondosan követjük az ajánlásokat, felfedezheted ezt a gyakran elfelejtett területet.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.