Практика обработки исключений в Python и расширениях на языке C

Раздел: Основы Python -> Обработка исключений

Обработка ошибок в 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;
}
- No installed python found (python не найден в системе)
- Python ignore error (игнорирование ошибок python)
- Python traceback using (трассировка ошибок в python)

Дополнительные примеры обработки исключений

Группы исключений в 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 закрывает доступ вне зависимости от результата.

Исключения в C и Python - comments

En
C python exceptions (python)