Грешки и изключения#
Понякога се случват неща, които не очакваме, или програмата ни влиза в състояние, което е грешно или пък е с недефинирано поведение. Например, ако се опитаме да отворим файл, който не съществува, или ако се опитаме да разделим число на 0 и т.н. Ако такава “грешка” е фатална, то е редно програмата да спре изпълнението си, съобщавайки за това с някаъв вид програмна грешка (error). Ако не е толкова фатална, например е просто специален случай, който можем да третираме по по-различен начин, то това наричаме “изключение” (exception). Ние като програмисти не е редно да “хващаме” и обработваме грешките, а само изключенията.
(Синтактични) Грешки#
В Python грешките биват единствено синтактични - от тип SyntaxError
. Тази грешка се хвърля когато Python parser-a забележи синтектичен проблем в кода, което би довело до невъзможността му за изпълнение.
print(("I like brackets")
Cell In [1], line 1
print(("I like brackets")
^
SyntaxError: incomplete input
Изключения#
Дори и кодът да е синтактично правилен обаче, изпълнението му е възможно да доведе до грешка. Такива грешки, които се засичат по време на изпълнението на програмата се наричат “изключения” (exceptions) и е възможно да бъдат “хванати” и обработени по желан от нас начин. Примери за често-срещани изключения:
42 / 0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In [2], line 1
----> 1 42 / 0
ZeroDivisionError: division by zero
"I love " + name_of_crush
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In [3], line 1
----> 1 "I love " + name_of_crush
NameError: name 'name_of_crush' is not defined
"1" + 1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [4], line 1
----> 1 "1" + 1
TypeError: can only concatenate str (not "int") to str
import math
math.sqrt(-1)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In [5], line 2
1 import math
----> 2 math.sqrt(-1)
ValueError: math domain error
l = [1, 2, 3]
l[3]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In [6], line 2
1 l = [1, 2, 3]
----> 2 l[3]
IndexError: list index out of range
d = {"a": 1, "b": 2}
d["c"]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In [7], line 2
1 d = {"a": 1, "b": 2}
----> 2 d["c"]
KeyError: 'c'
Note: въпреки, че са всъщност exceptions, а не errors, имената на доста вградени изключения завършват на Error
в Python.
Хващане на изключения#
С try-except
конструкцията можем да хванем изключение при изпълнение на даден код и да извършим някакво действие, ако се случи такова. В try
блокът се слага кодът, който искаме да изпълним, а в except
блокът - кодът, който искаме да се изпълни, ако се случи изключение. При хващане на изключение програмата не се терминира, а продължава изпълнението си след try-except
конструкцията.
while True:
try:
x = int(input("Please enter a number: "))
print("You entered: ", x)
break
except ValueError:
print("Sorry bro, that's not a valid number. Try again...")
Sorry bro, that's not a valid number. Try again...
Sorry bro, that's not a valid number. Try again...
You entered: 123
Забележете, че можем да укажем изрично типа на изключението, което искаме да хванем, като аргумент на except
блока. В горният пример искаме да хванем единствено изключения от тип ValueError
, т.е. проблеми с въведената стойност. Така позволяваме на потребителят например да приключи изпълнението на програмата с Ctrl+C (или подобен метод), понеже това действие би хвърлило KeyboardInterrupt
изключение.
Можем естествено и да не укажем тип, а да хванем абсолютно всички възможни изключения:
try:
risky_func()
except:
pass # never ever do this, for the sake of humanity
Много коварен antipattern обаче е обграждането на проблематичен код с except
блок, който е празен. Повече по темата: https://realpython.com/the-most-diabolical-python-antipattern/
Вместо това, може например да логнем грешката в някой файл или на стандартния изход (или по-добре от STDOUT - в STDERR). Има библиотеки, улесняващи логването, включително и вградена такава - logging
.
import logging
logger = logging.getLogger()
try:
risky_func()
except Exception as e:
logger.error(e)
name 'risky_func' is not defined
Както се вижда от примера, можем да присвоим изключението към променлива, която да използваме в except
блока. Това е полезно ако искаме да използваме изключението или аргументите на изключението, които се намират в args
атрибута на всяко изключение.
try:
risky_func()
except Exception as e:
logger.error(e.args)
("name 'risky_func' is not defined",)
Можем да очакваме повече от един тип изключения по няколко начина:
def f(): return 42
def g(): return 0
try:
# f()[2] # uncomment for third error
# y = h() # uncomment for second error
y = f() / g()
except ZeroDivisionError:
print("Division by zero.")
except NameError as e:
print("Name error: ", e)
except Exception as e:
print("Other error: ", e)
try:
y = f() / g() + h()
except (ZeroDivisionError, NameError) as e:
print("Oops: ", e)
Division by zero.
Oops: division by zero
Създателите на Python обичат да дефинират else
блокове за всевъзможни езикови контролни конструкции, и try
не е изключение (no pun intended). Кодът в else
след try-except
би се изпълнил само тогава, когато не е засечена никаква грешка при изпълнение.
try:
# y = 0 / 42
y = 42 / 0
except ZeroDivisionError:
print("Division by zero.")
else:
print("Everything is fine.")
Division by zero.
Освен това имаме и друг опционален блок - finally
. Той се изпълнява абсолютно винаги като последна част на try-(except)-(else)-finally
конструкцията, независимо от това дали е била прихваната грешка или не.
try:
# y = 0 / 42
y = 42 / 0
except ZeroDivisionError:
print("Division by zero.")
else:
print("Everything is fine.")
finally:
print("I'm always here. o.o")
Division by zero.
I'm always here. o.o

Хвърляне на изключения#
Изключенията наследяват класa Exception
. Списък от вграденитe такива може да откриете тук: https://docs.python.org/3/library/exceptions.html
Изключенията ги “хвърляме” с ключовата дума raise
:
raise Exception("This is an exceptionally exceptional exception.")
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
Cell In [15], line 1
----> 1 raise Exception("This is an exceptionally exceptional exception.")
Exception: This is an exceptionally exceptional exception.
raise ValueError # може и без извикването на конструктора от нас
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In [16], line 1
----> 1 raise ValueError
ValueError:
def get_name_by_id(id: int) -> str | None:
if not isinstance(id, int):
raise TypeError("id must be int!")
if id < 0:
raise ValueError("id must be a positive integer!")
database = ["Alex", "Lyubo", "Vankata"]
if id >= len(database):
return None
return database[id]
Те се пропагират нагоре по стека на изпълнението на програмата, докато не се срещне try
блок, който може да ги обработи. Ако няма такъв, програмата се терминира и се извежда съобщение за грешка. Това пропагиране е и причината да виждаме т.нар. Stacktrace:
def a(): raise Exception("Hello, stack!")
def b(): a()
def c(): b()
def d(): c()
def e(): d()
def f():
try:
e()
except:
print(f"Goodbye, stack. ;-;")
def g(): f()
def h(): g()
def i(): h()
i() # f will catch it
e() # nothing will catch it and kaboom
Goodbye, stack. ;-;
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
Cell In [18], line 20
15 def i(): h()
18 i() # f will catch it
---> 20 e()
Cell In [18], line 5, in e()
----> 5 def e(): d()
Cell In [18], line 4, in d()
----> 4 def d(): c()
Cell In [18], line 3, in c()
----> 3 def c(): b()
Cell In [18], line 2, in b()
----> 2 def b(): a()
Cell In [18], line 1, in a()
----> 1 def a(): raise Exception("Hello, stack!")
Exception: Hello, stack!
Можем да си дефинираме собствени изключения, като наследяваме класа Exception
(или някой от неговите наследници):
class InvalidPortionException(Exception):
def __init__(self, portion: tuple[str, str]) -> None:
super().__init__(*portion)
def __str__(self) -> str:
ingredients = " sus ".join(self.args)
return f"Ama kak taka, ne moje {ingredients}!"
class LelkataOtStolaDoNas:
def __init__(self):
osnovni = ("schnietzel", "kyufteta", "kartofeni kyufteta")
garnituri = ("kartofi", "oriz", "zele")
self.__allowed_portions = dict(zip(osnovni, garnituri)) # private shototo samo tq si gi znae...
def order(self, osnovno: str, garnitura: str) -> None:
portion = (osnovno, garnitura)
if portion not in self.__allowed_portions.items():
raise InvalidPortionException(portion)
print("Krem, airqn?")
lelkata = LelkataOtStolaDoNas()
lelkata.order("kyufteta", "kartofi")
---------------------------------------------------------------------------
InvalidPortionException Traceback (most recent call last)
Cell In [19], line 25
22 print("Krem, airqn?")
24 lelkata = LelkataOtStolaDoNas()
---> 25 lelkata.order("kyufteta", "kartofi")
Cell In [19], line 20, in LelkataOtStolaDoNas.order(self, osnovno, garnitura)
17 portion = (osnovno, garnitura)
19 if portion not in self.__allowed_portions.items():
---> 20 raise InvalidPortionException(portion)
22 print("Krem, airqn?")
InvalidPortionException: Ama kak taka, ne moje kyufteta sus kartofi!
Възможно е освен това да се chain-ват изключения, когато в except
блока се хвърли друго:
class UnbeknownstToMeException(Exception):
def __init__(self, fact: str) -> None:
msg = f"Е аз откъде да знам, че не може {fact}..."
super().__init__(msg)
lelka = LelkataOtStolaDoNas()
try:
lelka.order("kyufteta", "zele")
except InvalidPortionException as e:
portion = " със ".join(e.args)
raise UnbeknownstToMeException(portion)
---------------------------------------------------------------------------
InvalidPortionException Traceback (most recent call last)
Cell In [20], line 9
8 try:
----> 9 lelka.order("kyufteta", "zele")
10 except InvalidPortionException as e:
Cell In [19], line 20, in LelkataOtStolaDoNas.order(self, osnovno, garnitura)
19 if portion not in self.__allowed_portions.items():
---> 20 raise InvalidPortionException(portion)
22 print("Krem, airqn?")
InvalidPortionException: Ama kak taka, ne moje kyufteta sus zele!
During handling of the above exception, another exception occurred:
UnbeknownstToMeException Traceback (most recent call last)
Cell In [20], line 12
10 except InvalidPortionException as e:
11 portion = " със ".join(e.args)
---> 12 raise UnbeknownstToMeException(portion)
UnbeknownstToMeException: Е аз откъде да знам, че не може kyufteta със zele...
Disclaimer: историята е по действителен случай, obv.
За да индикираме, че дадено изключение е директно следствие на друго, може да използваме raise ... from ...
. Повече инфо в документацията.
assert
Когато искаме да предпазим изпълнението на код от недифинрано поведение, можем да използваме ключовата дума assert
. Тя проверява дали дадено условие е изпълнено и ако не е, то хвърля AssertionError
с някакво съобщение за грешка, което може да зададем ако искаме. Обикновено се използва на фаза разработка и тестване на код, но може и да се използва и в production код, ако сме сигурни, че няма никога да се изпълни и че ако все пак се изпълни, то е окей програмата ни да крашне.
from typing import Iterable
def evaluate(test_results: Iterable[int], actual_results: Iterable[int]) -> float:
assert len(test_results) == len(actual_results), "Expected and actual result series must be of equal length!"
return (sum((t - a) ** 2 for t, a in zip(test_results, actual_results)) / len(test_results)) ** 0.5 # RMSE
print(evaluate([1, 2, 3], [2, 2, 4]))
print(evaluate([1, 2, 3], [2, 2, 4, 5]))
0.816496580927726
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
Cell In [21], line 9
5 return (sum((t - a) ** 2 for t, a in zip(test_results, actual_results)) / len(test_results)) ** 0.5 # RMSE
8 print(evaluate([1, 2, 3], [2, 2, 4]))
----> 9 print(evaluate([1, 2, 3], [2, 2, 4, 5]))
Cell In [21], line 4, in evaluate(test_results, actual_results)
3 def evaluate(test_results: Iterable[int], actual_results: Iterable[int]) -> float:
----> 4 assert len(test_results) == len(actual_results), "Expected and actual result series must be of equal length!"
5 return (sum((t - a) ** 2 for t, a in zip(test_results, actual_results)) / len(test_results)) ** 0.5
AssertionError: Expected and actual result series must be of equal length!
В някои случаи можем да очакваме да прихванем и AssertionError
, примерно за да логнем това състояние, вместо да ни крашне програмата:
try:
evaluate([1, 2, 3], [2, 2, 4, 5])
except AssertionError as e:
logger.error(e)
Expected and actual result series must be of equal length!