Исключения и обработка ошибок
Не каждую проблему удобно решать через if: если что-то может пойти не так глубоко внутри
цепочки вызовов, а обработать это нужно совсем в другом месте, приходится либо пробрасывать
код ошибки через каждую промежуточную функцию, либо звать на помощь исключения
Исключения решают эту задачу - поднимаются в месте проблемы и перехватываются там, где для
них есть осмысленная реакция, минуя всё, что между ними
Теория
Зачем исключения, а не код возврата
Без исключений о неудаче обычно сигналят особым значением (None, -1, False) - и каждая
функция на пути наверх обязана его проверить и передать дальше:
def find_user(user_id: int) -> dict | None:
return DATABASE.get(user_id) # None, если не нашли
def get_user_email(user_id: int) -> str | None:
user = find_user(user_id)
if user is None:
return None # пробрасываем "неудачу" ещё на уровень выше
return user["email"]
Забыть одну такую проверку - и None тихо просочится туда, где его никто не ждёт, упав с
TypeError совсем в другом, не связанном с первопричиной месте
С исключением функция просто поднимает его в момент проблемы, а обработать можно там, где
для этого есть контекст - хоть на самом верху, хоть вообще нигде, если это фатально
try/except/else/finally - анатомия
def read_config(path: str) -> str:
try:
with open(path) as f:
content = f.read()
except FileNotFoundError:
print(f"{path} не найден, использую значения по умолчанию")
content = ""
else:
print(f"{path} успешно прочитан")
finally:
print("попытка чтения конфига завершена")
return content
Порядок выполнения:
try- код, который может кинуть исключениеexcept- выполняется, только если вtryподнялось подходящее исключениеelse- выполняется, только еслиtryотработал БЕЗ исключений (реже используется, но явно отделяет "основной путь" от обработки ошибки)finally- выполняется всегда: и при успехе, и при исключении, и даже если внутриtry/exceptвстретилсяreturn- типичное место для закрытия ресурсов, которое не должно зависеть от того, как завершился блок