В этой главе истории о дружбе C++ и Python будет на удивление мало использования Boost.Python. Передача исключений туда и обратно, по сути, является слабым местом этой библиотеки.
Мы будем использовать собственный API языка Python и там, где это возможно, будем использовать Boost.Python. Тем не менее, Boost.Python создает среду, в которой исключения из C++ поступают в Python в виде стандартного RuntimeError, а обратно из Python генерируется исключение C++ типа error_already_set, что означает «к вам что-то поступило, идите почитайте, что там».
сам.
" И здесь нам было бы полезно использовать C-API языка Python для чтения необходимой информации об исключении и преобразования ее в соответствующий класс в соответствии с логикой приложения.
Почему такие трудности? — Дело в том, что в Python, в отличие от C++, помимо текста исключения и его типа, есть еще трассировка — стек к месту возникновения исключения.
Давайте немного расширим стандартный std::Exception дополнительным параметром для этой трассировки стека, а заодно напишем двусторонний преобразователь исключений из классов C++ в классы исключений Python.
В предыдущих сериях
1. Буст.Питон.Введение.
Обертки C++ в Python.
2. Буст.Питон.Обертка для определенных классов C++.
3. Буст.Питон.
Создание преобразователей типов между C++ и Python.
Введение
Допустим, существует некая иерархия исключений, которую при обработке исключения необходимо представлять один к одному в виде соответствующего класса в C++ и в Python. Это особенно актуально, если вы комбинируете логику приложения на C++ со сложными сценариями Python или пишете модуль Python на C++ со сложной логикой.Рано или поздно мы сталкиваемся с обработкой исключения, поступающего из C++ в Python или наоборот. Конечно, в большинстве случаев вам будет достаточно стандартного механизма Boost.Python для преобразования исключения из C++ в Python в виде RuntimeException с текстом, поступающим изException::what().
На стороне C++ вам необходимо перехватить исключение типа error_already_set и использовать нативный Python API, но читать можно не только тип и текст исключения, но и трассировку — по сути, историю исключения.
Но обо всем по порядку.
Путь маленького исключения от C++ к Python
Итак, вы написали модуль на C++ с помощью Boost.Python, подключили его в коде Python через обычный Импортировать и используйте одну из оболочек функций или методов.Допустим, в коде C++ самое обычное исключение выбрасывается через столь же обычное исключение.
бросать .
В коде Python вы получите RuntimeException с текстом, возвращаемым изException::what(), если это исключение выдается из std::Exception. Если вас устраивает, что вы не получите ничего, кроме текста исключения, то вам даже больше ничего делать не придется.
Однако если вам нужно перехватить исключение для строго определенного класса ошибок, вам придется немного поработать.
Фактически Boost.Python предоставляет возможность зарегистрировать собственную трансляцию исключения, покидающего родину модуля, написанного на C++.
Все, что вам нужно сделать, это вызвать функцию boost::python::template в функции объявления модуля.
регистр_исключение_переводчик (F), где T — тип исключения в C++, а F — функция, которая принимает ссылку на исключение заданного типа и каким-то образом выполняет свою обязанность по передаче исключения нужного типа во внешний код уже на Python. В общем, примерно так:
Здесь мы использовали стандартный тип Exception, встроенный в Python, но вы можете использовать абсолютно любое исключение: стандартный , внешне подключенный через импорт с получением PyObject* через объект::ptr () или даже свой, созданный тут же на месте через PyErr_NewException .class error : public exception { .
}; .
void translate_error( error const& ); .
BOOST_PYTHON_MODULE( .
) { .
register_exception_translator<error>( translate_error ); } .
void translate_error( error const& e ) { PyErr_SetString( PyExc_Exception, e.what() ); }
Для завершения опыта добавим еще пару классов, которые будут использоваться как аналог ZeroDivisionError и ValueError и для полного счастья унаследуем их от нашей ошибки и назовем их нуль_division_error и value_error соответственно: class error : public exception
{
public:
error();
error( string const& message );
error( string const& message, string const& details );
virtual const char* what() const;
virtual string const& get_message() const;
virtual string const& get_details() const;
virtual const char* type() const;
private:
string m_message;
string m_details;
};
class value_error : public error
{
public:
value_error();
value_error( string const& message );
value_error( string const& message, string const& details );
virtual const char* type() const;
};
class zero_division_error : public error
{
public:
zero_division_error();
zero_division_error( string const& message );
zero_division_error( string const& message, string const& details );
virtual const char* type() const;
};
Поле m_details он понадобится нам на обратном пути от Python к C++, например, для сохранения обратной трассировки.
Метод type() понадобится для отладки чуть позже.
Простая и понятная иерархия.
Давайте зарегистрируем функции-трансляторы в Python для наших исключений: void translate_error( error const& );
void translate_value_error( value_error const& );
void translate_zero_division_error( zero_division_error const& );
.
BOOST_PYTHON_MODULE( .
) { .
register_exception_translator<error>( translate_error ); register_exception_translator<value_error>( translate_value_error ); register_exception_translator<zero_division_error>( translate_zero_division_error ); } .
void translate_error( error const& e )
{
PyErr_SetString( PyExc_Exception, e.what() );
}
void translate_value_error( value_error const& e )
{
PyErr_SetString( PyExc_ValueError, e.what() );
}
void translate_zero_division_error( zero_division_error const& e )
{
PyErr_SetString( PyExc_ZeroDivisionError, e.what() );
}
Отлично, осталось только создать тестовые функции на стороне C++, которые будут выдавать такие исключения: double divide( double a, double b )
{
if( abs( b ) < numeric_limits<double>::epsilon() )
throw zero_division_error();
return a / b;
}
double to_num( const char* val )
{
double res;
if( !val || !sscanf( val, "%LG", &res ) )
throw value_error();
return res;
}
void test( bool val )
{
if( !val )
throw error( "Test failure.", "test" );
}
Почему бы и нет, эти функции не хуже любых других и дают именно то, что нам нужно.
Давайте завершим их: .
BOOST_PYTHON_MODULE( python_module ) { register_exception_translator<error>( translate_error ); register_exception_translator<value_error>( translate_value_error ); register_exception_translator<zero_division_error>( translate_zero_division_error ); def( "divide", divide, args( "a", "b" ) ); def( "to_num", to_num, args( "val" ) ); def( "test", test, args( "val" ) ); } .
Что ж, давайте соберем наш модуль и выполним импортировать python_module , вызываем наши функции с нужными параметрами, получаем нужные исключения (скрипт на Python 3.x): import python_module as pm
try:
res = pm.divide( 1, 0 )
except ZeroDivisionError:
print( "ZeroDivisionError - OK" )
except Exception as e:
print( "Expected ZeroDivisionError, but exception of type '{t}' with text: '{e}'".
format(t=type(e),e=e) ) else: print( "Expected ZeroDivisionError, but no exception raised! Result: {r}".
format(r=res) ) try: res = pm.to_num( 'qwe' ) except ValueError: print( "ValueError - OK" ) except Exception as e: print( "Expected ValueError, but exception of type '{t}' with text: '{e}'".
format(t=type(e),e=e) ) else: print( "Expected ValueError, but no exception raised! Result: {r}".
format(r=res) ) try: res = pm.test( False ) except Exception as e: if type(e) is Exception: print( "Exception - OK" ) else: print( "Exception of type '{t}', expected type 'Exception', message: '{e}'".
format(t=type(e),e=e) ) else: print( "Expected Exception, but no exception raised! Result: {r}".
format(r=res) )
Вывод скрипта:
ZeroDivisionError — ОК
Ошибка значения — ОК
Исключение – ОК
Все идет нормально.
Пойдем в противоположном направлении.
Приключения нашего исключения на пути от Python к C++
Выделим типы исключений и тестовые функции в отдельный проект и соберем их в отдельную динамически подключаемую библиотеку.типы_ошибок .
Модуль для Python мы соберем отдельно в проекте.
python_module .
Теперь создадим приложение на C++, в котором мы будем перехватывать исключения из Python, назовем его catch_Exceptions .
Все, что вам нужно сделать, это подключить наш модуль через import("python_module"), а затем получить доступ к функциям модуля через attr("divide"), attr("to_num"), attr("test").
Мы их вызовем, они сгенерируют исключения на уровне кода C++, перейдут в интерпретатор Python и перенаправятся дальше в приложение C++, вызывая исключение.
error_already_set — исключение из библиотеки Boost.Python, подготовленное именно для таких случаев.
Сам объект типа error_already_set , важно просто перехватить исключение.
В целом обработка такого исключения выглядит так: catch( error_already_set const& )
{
PyObject *exc, *val, *tb;
PyErr_Fetch( &exc, &val, &tb );
PyErr_NormalizeException( &exc, &val, &tb );
handle<> hexc(exc), hval( allow_null( val ) ), htb( allow_null( tb ) );
throw error( extract<string>( !hval ? str( hexc ) : str( hval ) ) );
}
Таким образом, мы всегда будем получать исключение одного и того же типа, но, по крайней мере, сможем извлечь текст исключения.
Но мы получаем тип исключения в переменной exc, сам объект исключения в переменной val и даже объект со стеком возникшего исключения в переменной tb. Давайте преобразуем возникшее исключение в нуль_division_error и value_error, если были получены ZeroDivisionError или ValueError соответственно.
Останавливаться! Не все понимают, что это за две функции, почему всё PyObject*, почему в C-API есть исключения, если их нет в C, давайте подробнее.
Да, в чистом C исключений нет, но в Python они есть, и его API предоставляет возможность получить информацию о возникшем исключении.
В Python C-API все значения и типы, а по сути почти всё, представлены как PyObject*, поэтому исключение E типа T — это пара значений типа PyObject*, добавим к этому PyObject* для трассировки — сохраненный стек, где произошло исключение.
Получить информацию о произошедшем исключении можно с помощью функции PyErr_Fetch , после чего информацию об исключении можно нормализовать (если вы не хотите работать во внутреннем представлении в виде кортежа) с помощью функции PyErr_NormalizeException .
После вызова пары этих функций мы заполним три значения типа PyObject* соответственно: класс исключения, экземпляр (объект) исключения и стек (traceback), сохраненный на момент возникновения исключения.
брошен.
Далее, гораздо удобнее работать с Boost.Python; мы оборачиваем PyObject* в boost::python:: ручка <> , который совместим с любым объектом библиотеки Boost.Python, нам просто нужен boost::python:: ул.
.
После преобразования в строковый эквивалент Python в Boost.Python мы можем получить стандартный собственный стандарт C++ std::string. При желании вы также можете извлечь обычный const char*.
С первыми двумя параметрами всё понятно, они прекрасно преобразуются в строку, но с помощью трассировки всё равно придётся преобразовывать её в читаемую форму.
Самый простой способ сделать это — использовать модуль Выслеживать передав наши три параметра в функцию формат_исключение .
Функция трассировки.
format_Exception(exc, val, tb) вернет нам массив строк в виде стандартного список Язык Python, который прекрасно объединяется в одну большую жирную линию.
На стороне C++ при использовании Boost.Python это будет выглядеть примерно так: .
format_exception = import( "traceback" ).
attr( "format_exception" ); return extract<string>( str( "" ).
join( format_exception( exc, val, tb ) ) );
}
Вы можете создать вспомогательную функцию для генерации строки из исключения.
Проблема, однако, в том, что вызов import() в функции каждый раз приведет к дорогостоящему вызову, поэтому объект, полученный с помощью import( "traceback").
attr( "format_Exception"), лучше всего хранить в отдельном объекте; нам также нужно сохранить результат import("python_module").
Учитывая, что это нужно будет сделать где-то между Py_Initialize() и Py_Finalize(), на ум не приходит ничего лучше, чем одноэлементные поля для хранения таких переменных.
Работа с API Python через Singleton
Итак, создадим синглтон, это усложнит приложение, но несколько упростит код и позволит корректно инициализировать работу с интерпретатором, сохранить все вспомогательные объекты и все корректно выполнить: class python_interpreter
{
public:
static double divide( double, double );
static double to_num( string const& );
static void test( bool );
static string format_error( handle<> const&, handle<> const&, handle<> const& );
private:
object m_python_module;
object m_format_exception;
python_interpreter();
~python_interpreter();
static python_interpreter& instance();
object& python_module();
string format_error( object const&, object const&, object const& );
};
Конструктор инициализирует работу с интерпретатором, а деструктор очистит сохраненные поля и деинициализирует работу с интерпретатором; методы python_module и format_error импортируют соответствующие модули только один раз:
python_interpreter::python_interpreter()
{
Py_Initialize();
}
python_interpreter::~python_interpreter()
{
m_python_module = object();
m_format_exception = object();
Py_Finalize();
}
double python_interpreter::divide( double a, double b )
{
return extract<double>( instance().
python_module().
attr("divide")( a, b ) );
}
double python_interpreter::to_num( string const& val )
{
return extract<double>( instance().
python_module().
attr("to_num")( val ) );
}
void python_interpreter::test( bool val )
{
instance().
python_module().
attr("test")( val );
}
string python_interpreter::format_error( handle<> const& exc, handle<> const& msg, handle<> const& tb )
{
return instance().
format_error( object(exc), object(msg), object(tb) );
}
python_interpreter& python_interpreter::instance()
{
static python_interpreter single;
return single;
}
object& python_interpreter::python_module()
{
if( m_python_module.is_none() )
m_python_module = import( "python_module" );
return m_python_module;
}
string python_interpreter::format_error( object const& exc, object const& val, object const& tb )
{
if( m_format_exception.is_none() )
m_format_exception = import( "traceback" ).
attr( "format_exception" );
return extract<string>( str( "" ).
join( m_format_exception( exc, val, tb ) ) );
}
Итого, мы имеем готовый механизм, применимый к любому C++-приложению, использующему Python в качестве мощного вспомогательного функционала с кучей библиотек.
Пришло время протестировать наш механизм исключений!
Проверка механизма трансляции исключений с Python на C++
Создадим вспомогательную функцию: void rethrow_python_exception()
{
PyObject *exc, *val, *tb;
PyErr_Fetch( &exc, &val, &tb );
PyErr_NormalizeException( &exc, &val, &tb );
handle<> hexc(exc), hval( allow_null( val ) ), htb( allow_null( tb ) );
string message, details;
message = extract<string>( !hval ? str( hexc ) : str( hval ) );
details = !tb ? extract<string>( str( hexc ) ) : python_interpreter::format_error( hexc, hval, htb );
if( PyObject_IsSubclass( exc, PyExc_ZeroDivisionError ) )
throw zero_division_error( message, details );
else if( PyObject_IsSubclass( exc, PyExc_ValueError ) )
throw value_error( message, details );
else
throw error( message, details );
}
Тогда механизм обработки исключений сведется к следующей схеме для каждого тестируемого метода, например для деления:
try
{
try
{
python_interpreter::divide( 1, 0 );
}
catch( error_already_set const& )
{
rethrow_python_exception();
}
}
catch( error const& e )
{
output_error( e );
}
Здесь output_error — простейшая функция, выводящая информацию об исключении, например так:
void output_error( error const& e )
{
cerr << "\nError type: " << e.type() << "\nMessage: " << e.get_message() << "\nDetails: " << e.get_details() << endl;
}
Вот тут-то и пригодился виртуальный метод type(), который мы создали в ошибке базового класса.
Создаём аналогичные секции для to_num и для test, а также проверяем, что получится, если просто выполнить строку «1/0» в Python через exec: try
{
try
{
exec( "1 / 0" );
}
catch( error_already_set const& )
{
rethrow_python_exception();
}
}
catch( error const& e )
{
output_error( e );
}
Запустим.
Вывод должен быть примерно таким: Тип ошибки: Zero_division_error Сообщение: Деление на ноль! Подробности: Тип ошибки: value_error Сообщение: Недопустимое значение! Подробности: Тип ошибки: ошибка Сообщение: Ошибка теста.
Подробности:
Тип ошибки: Zero_division_error.
Сообщение: деление на ноль
Подробности: обратная трассировка (последний вызов):
Файл "", строка 1, в ZeroDivisionError: деление на ноль
Эпилог
Итого: у нас есть механизм однозначного преобразования исключений из Python в C++ и наоборот. Минус заметен сразу – это разные, совершенно не связанные друг с другом сущности.Это связано с тем, что класс C++ не может быть унаследован от класса Python, и наоборот. Есть возможность «инкапсулировать» необходимый класс исключений в обертку класса C++ для Python, но это все равно будут разные классы, которые просто конвертируются в соответствующие, каждый на своем языке.
Если у вас сложная иерархия исключений в C++, проще всего создать аналог в Python в отдельном модуле .
py, поскольку создать его можно с помощью PyErr_NewException , а потом хранить его где-то довольно дорого и не добавит читабельности коду.
Не знаю, как вы, а я с нетерпением жду, когда Boost.Python получит достойный транслятор исключений или хотя бы аналог boost::python::bases для наследования обертки от класса Python. В целом Boost.Python — отличная библиотека, но этот аспект добавляет геморроя при разборе исключений из Python на стороне C++.
Перевод на Python через регистрацию функции-переводчика Register_Exception_translator (F) выглядит вполне удачно и позволяет конвертировать исключение типа A в C++ в совершенно другой класс B на стороне Python, но хотя бы автоматически.
В принципе, на error_already_set реагировать точно так, как описано выше, не обязательно; вы можете выбрать свой собственный рецепт поведения вашего приложения, используя API Python для обработки исключений из Python .
Ссылка на проект есть здесь (~223 КБ) .
Проект MSVS v11 настроен для сборки с помощью Boost 1.52 и Python 3.3 x64.
Полезные ссылки
PyWiki — получение исключений на стороне C++ Обработка исключений с помощью Python C-API Регистрация транслятора исключений с C++ на Python Теги: #C++ #c++11 #python #boost::python #module #hybrid #script #python3 #Exception #Exception #translation #wrapper #embedded #python #programming #C++-
Ibm Приобрела Merge Healthcare За $1 Млрд.
19 Oct, 24 -
Ограничения На Публикацию – Борьба Со Спамом
19 Oct, 24 -
Альтернативная Классификация Ошибок
19 Oct, 24