try/except/finally на практике: конкретные исключения вместо errno-стиля и bare except
core написать обработку ошибок для операции ввода-вывода, которая перехватывает только ожидаемые исключения и гарантированно освобождает ресурс
В C очистка ресурсов размазана по коду через goto cleanup и повторяющиеся if (err) { free(...); return -1; } на каждом уровне — это способ гарантировать, что fclose/free вызовутся на всех путях выхода из функции. Python убирает эту ручную бухгалтерию через try/except/finally и контекстные менеджеры (with), но перенос C-привычек сюда рождает две характерные ошибки.
Ошибка первая: голый except:. Си-программист привык, что «поймать всё» — это safety net, аналог проверки if (ret != 0) без разбора кода ошибки. В Python except: без указания типа перехватывает всё, что наследуется от BaseException — включая KeyboardInterrupt (Ctrl+C) и SystemExit (вызов sys.exit()). Программа, которая не реагирует на Ctrl+C и не может корректно завершиться по sys.exit(), — прямое следствие этой привычки. Чуть менее опасный, но всё равно вредный вариант — except Exception: без разбора: он маскирует программные ошибки (TypeError, AttributeError из-за опечатки) точно так же, как в C маскирует баг привычка писать if (ret) { /* просто игнорируем */ }.
Ошибка вторая: считать, что finally — это goto cleanup, поэтому можно не думать о том, какие исключения вообще возможны. finally гарантирует выполнение блока при любом исходе — исключение, return, break — но это не индульгенция на то, чтобы не указывать конкретные типы в except. finally отвечает за освобождение, except — за решение, что делать с конкретной ошибкой. Это разные обязанности, и в C они смешаны в одном if.
Структура для операции ввода-вывода, написанная правильно:
resource = acquire_resource()
try:
result = do_io(resource)
except (ConnectionError, TimeoutError) as e:
log.error("I/O failed: %s", e)
raise
finally:
resource.release()
Обратите внимание: finally выполнится и при успехе, и при перехваченном исключении, и даже если except-блок сам решит поднять исключение повторно (raise без аргумента передаёт оригинальное исключение дальше с сохранённым traceback — чего errno-стиль вообще не умеет).
Важный нюанс, которого нет в C: except-блоки проверяются сверху вниз, и перехватывается первый подходящий по иерархии тип. Значит except Exception перед except ValueError в том же блоке сделает второй недостижимым — молчаливая логическая ошибка, которую интерпретатор не подсветит.
Разобранный пример
Задача: прочитать JSON-конфиг из ресурса, обработать отсутствие данных и повреждённый JSON отдельно, и в любом случае освободить ресурс (разберём случай, когда ресурс — не файл с with, а, скажем, сетевое соединение).
import json
def load_config(path):
conn = acquire_connection(path) # условный "ресурс", не файл
try:
raw = conn.read_all()
return json.loads(raw)
except FileNotFoundError:
log.warning("config %s not found, using defaults", path)
return {}
except json.JSONDecodeError as e:
log.error("config %s is corrupted: %s", path, e)
raise
finally:
conn.close()
Рассуждение: FileNotFoundError и json.JSONDecodeError — это два разных по смыслу отказа с разной реакцией (один можно тихо заглушить дефолтом, другой нужно поднять дальше, потому что повреждённый конфиг молча игнорировать нельзя). conn.close() в finally выполнится в обоих случаях и при успехе — ровно так, как в C вы гарантировали бы fclose через единственный goto cleanup, но здесь это встроено в структуру языка и не размножается по функции.
Попробуй сейчас
У вас есть функция, которая открывает сетевой сокет (условно sock = open_socket(addr)), отправляет запрос (sock.send(data)) и читает ответ (sock.recv()). Возможные ошибки: ConnectionRefusedError при подключении, TimeoutError при чтении ответа. Напишите обработку так, чтобы:
- оба типа ошибок логировались с разным сообщением;
- сокет закрывался в любом случае, включая случай, когда ошибка не входит ни в один из двух типов (например,
KeyboardInterrupt); - неожиданные исключения не проглатывались.
Получилось, если…
Проверьте себя: в вашем коде нет except: и нет except Exception: без последующего raise; sock.close() вызывается ровно один раз в finally; если временно закомментировать оба except и подставить искусственный ValueError, программа падает с трейсбеком, а не тихо продолжает работу.
flowchart TD
Base[BaseException] --> Exc[Exception]
Base --> KI[KeyboardInterrupt]
Base --> SE[SystemExit]
Exc --> OSE[OSError]
Exc --> VE[ValueError]
Exc --> TE[TypeError]
OSE --> FNF[FileNotFoundError]
OSE --> TO[TimeoutError]
OSE --> CE[ConnectionError]
CE --> CR[ConnectionRefusedError]
Bare[bare except: перехватывает всё дерево] -.-> BaseВывод
except: без типа — это не «поймать все ошибки», а «поймать даже сигналы завершения программы». Указывайте конкретные типы исключений, оставляйте finally только для освобождения ресурсов, и не путайте порядок except-блоков — он проверяется последовательно, а не по «наиболее подходящему» типу.
AI-generated · source-grounded review
🛡 Fact-checked: 0 risky claims verified · 1 removed · confidence: high · figures: 1
⚑ Second opinion disputes this lesson
A second, independent model re-checked the fact-check and disagreed on at least one point below. Do not read this lesson as fully verified.
On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course