Практика обработки исключений в Python и расширениях на языке C
Обработка ошибок в C и Python
В C нет встроенного понятия исключения. Программа на C возвращает целочисленный код и может записать подробности в errno. В Python исключения являются объектами, а работа с ними встроена в синтаксис языка. CPython написан на C, поэтому внутри интерпретатора исключения Python хранятся как структуры PyObject и управляются функциями C API.
Базовый приём try/except в Python
Главный способ обработки ошибок в Python, используемый в большинстве программ, это конструкция try/except. Механизм встроен в язык и не требует проверки кода после каждого вызова.
try:
value = input()
number = int(value)
result = 10 / number
except ValueError:
print("Нечисловое значение")
except ZeroDivisionError:
print("Деление на ноль")
else:
print("Результат", result)
finally:
print("Завершение")Client error python (ошибка http-клиента в python)
Введите значение: 0 Деление на ноль Завершение
Script not found python (ошибка 'script not found')
Несколько except позволяют обрабатывать разные классы по отдельности. Блок else выполняется после удачного выполнения try, а finally выполняется всегда. Такой подход удобен при работе с вводом, файлами, сетевыми запросами и любыми операциями, которые могут завершиться сбоем.
Пустой except без класса перехватывает все ошибки, включая SystemExit и KeyboardInterrupt. Поэтому его использование может скрыть остановку программы.
Как поднять исключение Python из C кода?
Расширения CPython реализуются на C и должны сообщать интерпретатору об ошибках. Для этого существует семейство функций PyErr_*. Функция, которая должна возбудить исключение, после установки ошибки возвращает NULL.
#include <Python.h>
static PyObject *py_divide(PyObject *self, PyObject *args) {
double a, b;
if (!PyArg_ParseTuple(args, "dd", &a, &b)) {
return NULL;
}
if (b == 0.0) {
PyErr_SetString(PyExc_ZeroDivisionError,
"деление на ноль в C расширении");
return NULL;
}
return PyFloat_FromDouble(a / b);
}Python exception (исключения в python)
Здесь при неудачном разборе аргументов PyArg_ParseTuple сама устанавливает TypeError. При делении на ноль PyErr_SetString создаёт ZeroDivisionError с переданным сообщением. Интерпретатор Python видит NULL и превращает это в исключение, которое можно перехватить try/except.
Как в C обработать ошибку без исключений?
В системном программировании часто используется код возврата и errno. Функция fopen возвращает NULL, если файл не открыт, а errno содержит код причины.
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(void) {
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
int error = errno;
printf("Ошибка: %s\n", strerror(error));
return error;
}
fclose(file);
return 0;
}Python errno (обработка ошибки errno в python)
Такой вариант применяется в библиотеках Python, написанных на C, когда ошибка не должна быть исключением. Код вызывающего участка должен явно проверять каждое возвращаемое значение. Это громоздко, но предсказуемо для C программ.
Как эмулировать исключения в C с помощью setjmp и longjmp?
Набор функций setjmp и longjmp позволяет выполнить переход из глубокой вложенности в заранее заданную точку. Такой переход похож на исключение, но не создаёт объектов исключений.
#include <stdio.h>
#include <setjmp.h>
static jmp_buf env;
static void risky_function(void) {
longjmp(env, 10);
}
int main(void) {
int value = setjmp(env);
if (value == 0) {
risky_function();
printf("Нет ошибки\n");
} else {
printf("Код ошибки: %d\n", value);
}
return 0;
}Python no module named pip (ошибка 'no module named pip' в python)
Код ошибки: 10
Python externally managed environment (ошибка externally managed environment в python)
Состояние после longjmp похоже на исключение, но память и другие ресурсы, выделенные между setjmp и longjmp, не освобождаются автоматически. Локальные переменные, изменившиеся после setjmp, могут иметь неопределённое значение, если они не объявлены volatile. Поэтому такой приём применяют только в виде исключения, когда время жизни всех ресурсов точно контролируется.
Как перехватить исключение Python внутри C программы?
C программа, встраивающая Python, вызывает Python объекты и должна распознавать исключения. Проверка результата на NULL даёт сигнал, а PyErr_Fetch извлекает тип, значение и traceback.
#include <Python.h>
void call_python_function(PyObject *func) {
PyObject *result = PyObject_CallObject(func, NULL);
if (result == NULL) {
PyObject *type, *value, *tb;
PyErr_Fetch(&type, &value, &tb);
PyErr_NormalizeException(&type, &value, &tb);
PyObject *str = PyObject_Str(value);
const char *text = PyUnicode_AsUTF8(str);
printf("Поймано исключение: %s\n", text);
Py_XDECREF(type);
Py_XDECREF(value);
Py_XDECREF(tb);
Py_XDECREF(str);
} else {
Py_DECREF(result);
}
}List out of range python (ошибка indexerror: list index out of range в python)
PyErr_Fetch забирает указатели из состояния потока. PyErr_NormalizeException приводит значение к экземпляру класса, если это необходимо. После преобразования в UTF-8 строку сообщение печатается, а все ссылки освобождаются, чтобы не возникло утечки.
Как создать собственный класс исключения в Python?
Исключения можно определять как подклассы Exception. Собственный класс помогает отделить бизнес-ошибки от системных.
class NotEnoughMoney(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise NotEnoughMoney("доступно " + str(balance))
return balance - amount
try:
withdraw(500, 700)
except NotEnoughMoney as exc:
print(exc)Python error code 1 (ошибка python с кодом 1)
доступно 500
Line in module python ошибка (ошибка в строке модуля python)
Такой класс полезен в библиотеках, где нужно ловить только свои ошибки. Встроенные исключения остаются для нестандартных ситуаций и ошибок интерпретатора.
Как создать собственное исключение для модуля на C?
В CPython создание исключения выполняется функцией PyErr_NewException. Затем объект добавляется в модуль.
#include <Python.h>
static PyObject *MyError;
static PyObject *raise_my_error(PyObject *self, PyObject *args) {
PyErr_SetString(MyError, "пользовательская ошибка из C");
return NULL;
}
static PyMethodDef methods[] = {
{"raise_my_error", raise_my_error, METH_NOARGS, "Создаёт исключение MyError"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef my_module = {
PyModuleDef_HEAD_INIT, "my_module", NULL, -1, methods
};
PyMODINIT_FUNC PyInit_my_module(void) {
PyObject *module = PyModule_Create(&my_module);
MyError = PyErr_NewException("my_module.MyError", NULL, NULL);
Py_INCREF(MyError);
PyModule_AddObject(module, "MyError", MyError);
return module;
}Python codec can t decode byte (ошибка декодирования байтов в python)
Py_INCREF сохраняет глобальную ссылку после PyModule_AddObject. Если пропустить Py_INCREF, указатель MyError может стать висячим после завершения функции инициализации модуля.
Типичная ошибка: слишком широкий except
Перехват всех исключений затрудняет поиск причины сбоя. Нужно указывать классы, которые программа способна обработать. Для непредвиденных ошибок лучше дать программе остановиться или записать traceback.
try:
data = load()
except PermissionError:
data = None
except (ValueError, TypeError):
data = []Attributeerror module has no attribute python (attributeerror: модуль не имеет атрибута в python)
Проблема с return и finally
Если finally содержит return, он отменяет исключение. Это часто становится неожиданностью.
def f():
try:
raise ValueError("сбой")
finally:
return 42
print(f())ошибка типа данных python (ошибка типа данных в python)
42
C python exceptions (исключения в c и python)
Вместо return в finally лучше использовать finally для освобождения ресурсов и не менять поток выполнения.
Типичная ошибка в C расширении: NULL без исключения
При возврате NULL из функции, вызванной из Python, обязательно должно быть установлено исключение. Если PyErr_SetString не вызывался, Python сообщит SystemError. Поэтому перед return NULL нужно проверять PyErr_Occurred.
if (result_is_error) {
if (!PyErr_Occurred()) {
PyErr_SetString(PyExc_RuntimeError, "причина сбоя");
}
return NULL;
}Дополнительные примеры обработки исключений
Группы исключений в Python 3.11
Механизм ExceptionGroup и except* позволяет обрабатывать несколько одновременных ошибок.
try:
raise ExceptionGroup("набор ошибок",
[ValueError("неверное значение"),
TypeError("неверный тип")])
except* ValueError as eg:
print("ValueError:", eg.exceptions)
except* TypeError as eg:
print("TypeError:", eg.exceptions)ValueError: (ValueError('неверное значение'),)
TypeError: (TypeError('неверный тип'),)except* перехватывает все исключения соответствующего класса в группе и передаёт новый объект ExceptionGroup. Это полезно при параллельных вычислениях, когда несколько задач завершились ошибками.
Причинная связь исключений
Конструкция raise ... from ... устанавливает атрибут __cause__.
class BusinessError(Exception):
pass
try:
try:
x = int("abc")
except ValueError as cause:
raise BusinessError("не удалось преобразовать данные") from cause
except BusinessError as error:
print("Внешняя ошибка:", error)
print("Причина:", error.__cause__)Внешняя ошибка: не удалось преобразовать данные Причина: invalid literal for int() with base 10: 'abc'
__cause__ хранит исходное исключение, что упрощает диагностику. Если from не используется, интерпретатор сам добавит контекст, но явная запись точнее.
Игнорирование исключений с contextlib.suppress
Для случаев, когда ошибку нужно сознательно пропустить, удобен класс suppress.
from contextlib import suppress
with suppress(FileNotFoundError, PermissionError):
text = open("config.txt", "r").read()
print("Файл прочитан")
print("Программа продолжила работу")Программа продолжила работу
Этот способ заменяет try/except с pass, но важно не злоупотреблять им, потому что пропадает информация об ошибке.
PyErr_Format для формирования текста исключения в C
Функция PyErr_Format работает похоже на printf и подставляет значения в строку ошибки.
static PyObject *py_check_code(PyObject *self, PyObject *args) {
int code;
if (!PyArg_ParseTuple(args, "i", &code)) {
return NULL;
}
if (code < 0) {
PyErr_Format(PyExc_ValueError,
"код %d меньше нуля", code);
return NULL;
}
return PyLong_FromLong(code * 2);
}В Python такой вызов приведёт к ValueError с текстом, в который подставлено переданное число. PyErr_Format удобна, когда сообщение зависит от конкретных данных.
Игнорирование и сброс исключения внутри C программы
Если C программа вызывает Python функцию и не хочет передавать исключение дальше, можно использовать PyErr_Clear.
PyObject *callable = get_callable();
PyObject *result = PyObject_CallObject(callable, NULL);
if (result == NULL) {
PyErr_Clear();
PyObject *fallback = PyLong_FromLong(0);
result = fallback;
}
Py_DECREF(result);PyErr_Clear удаляет установленное исключение и очищает память. После этого состояние интерпретатора снова становится нормальным. Этот приём применяется, когда сбой не критичен и требуется значение по умолчанию.
StopIteration и значение return в генераторе
Когда генератор завершается инструкцией return, значение сохраняется в атрибуте value исключения StopIteration.
def values():
yield 1
return 42
generator = values()
print(next(generator))
try:
next(generator)
except StopIteration as stop:
print("Возвращено", stop.value)1 Возвращено 42
Такое поведение используется при реализации протокола итератора, а также в функциях, возвращающих первое значение через next().
Перехват вложенных исключений при работе с файлами
Наконец, пример сочетания try, except, else и finally при работе с файлом.
try:
file = open("settings.ini", "r")
except FileNotFoundError:
print("Файл не найден")
except PermissionError:
print("Нет доступа к файлу")
else:
with file:
for line in file:
print(line.rstrip())
finally:
print("Операция завершена")Файл не найден Операция завершена
В блоке else файл обрабатывается только после успешного открытия. finally закрывает доступ вне зависимости от результата.