() 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 StopIteration
, StandardError
é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.