Данная статья является продолжением первой части.
Продолжаем мучить Boost.Python. В этот раз настала очередь класса, который нельзя ни создать, ни скопировать.
Обернём почти обычную сишную структуру с необычным конструктором.
И поработаем с возвращением ссылки на поле объекта C++, так чтобы сборщик мусора Python его не удалил ненароком. Ну и наоборот, сделаем альтернативный вариант, чтобы Python прибрал мусор после удаления того, что ему отдали на хранение.
Поехали…
Нам для наших целей вполне хватит дополнить проект example оставшийся с предыдущей части.
Давайте добавим в него ещё пару файлов для работы с классом синглтона:
single.h
single.cpp
И вынесем объявления вспомогательных функций для обёртки в Python в отдельный файл:
wrap.h
От прежнего проекта должен был остаться файл, который мы активно будем менять:
wrap.cpp
И замечательные файлы с чудо-классом, который так нам помог в первой части, они останутся как есть:
some.h
some.cpp
Начнём с того, что заведём в single.h небольшую C-style структуру, просто с описанием полей.
Давайте для интереса это будет не просто структура, а некий загадочный тип описания конфигурации:
Сделать обёртку для такой структуры не составит никакого труда, нужно лишь специально описать конструктор с параметрами с помощью шаблона конструктора boost::python::init<...>(...) параметра шаблона обёртки boost::python::class_:
Как видите, здесь даже не приходится применять return_value_policy<copy_const_reference> для строкового поля. Просто потому, что поле здесь берётся по сути по значению, а стало быть автоматически преобразуется в стандартную строку str языка Python.
Функции make_setter делают ещё очень полезную работу по проверке типа входящего значения, попробуйте например присвоить в питоне полю coef значение строкового типа или задать max_size значением типа float, получите исключение.
Поля сишной структуры Config по сути превращаются в свойства объекта полноценного питоновского класса Config. Ну почти полноценного… Давайте по аналогии с классом Some из прошлой главы добавим в обёртку методы __str__ и __repr__, а заодно добавим свойство as_dict для преобразования полей структуры в стандартный dict питона и обратно.
Объявление новых функций, так же как и старых, перенесём в наш новый файл wrap.h:
В файле wrap.cpp не останется ничего лишнего и сразу же будет объявление модуля example, что явно прибавит читабельности.
В конце wrap.cpp напишем реализацию наших новых функций, по аналогии с тем, как мы их писали в первой части:
Это я конечно уже с жиру бешусь, но назовём это повторением пройденного.
В обёртку структуры разумеется добавляем новые объявления:
Со структурой ничего сложного, из неё получился замечательный питоновский класс, зеркально повторяющий свойства структуры Config в C++ и при этом класс вполне питонист. Единственная проблема с данным классом будет в том, что в конструктор при создании надо будет что-нибудь указать.
Для заполнения параметров конфигурации и доступа к ним давайте заведём синглтон, заодно снабдим его «полезным» счётчиком.
Итак синглтон. Пусть в нём будут содержаться вышеупомянутые параметры конфигурации текущего приложения и некий волшебный счётчик для получения текущего идентификатора.
Как вы наверное успели заметить, я не очень-то люблю вытаскивать в секцию public бесполезный метод Instance() и предпочитаю работать с функционалом синглтона как с набором статических методов. От этого синглтон не перестаёт быть синглтоном, а пользователь класса скажет вам спасибо, за то, что спрятали вызов Instance() в реализацию.
Вот собственно и она, реализация в файле single.cpp:
Всего три статичных метода, обёртка не должна получиться сложной, если не учитывать одно но… хотя нет, не совсем одно:
1. Нельзя создавать экземпляр класса
2. Нельзя копировать экземпляр класса
3. Мы ещё не оборачивали статические методы
Как видите все сложности связанные с 1-м и 2-м пунктам сводятся к указанию параметра шаблона boost::noncopyable и передаче параметра boost::python::no_init конструктору шаблонного класса boost::python::class_.
Если требуется, чтобы класс поддерживал копирование или содержал конструктор по умолчанию, можете стереть соответствующий глушитель генератора свойств класса-обёртки.
Вообще говоря конструктор по умолчанию можно объявить ниже в .def( init<>() ), некоторые так и делают, для единообразия с другими конструкторами с параметрами, описываемыми отдельно, также передавая no_init в конструктор шаблона обёртки. Есть также вариант с заменой конструктора по-умолчанию на конструктор с параметрами прямо при объявлении класса-обёртки, как это мы уже сделали для структуры Config.
С третьим пунктом вообще всё просто, объявлением того, что метод статичный занимается .staticmethod() после объявления через .def() обёртки всех перегрузок данного метода.
В общем-то остальное уже не вызывает вопросов и знакомо нам по первой части, кроме одной забавной мелочи — политики возвращаемого по ссылке значения return_value_policy<reference_existing_object>, о ней далее.
Самую большую сложность в обёртке методов нашего синглтона вызвало возвращение ссылки на объект из метода
Просто для того, чтобы Garbage Collector (GC) интерпретатора Python не удалил содержимое поля класса возвращённого по ссылке из метода, мы используем return_value_policy<reference_existing_object>.
Магия Boost.Python настолько сурова, что при выполнении кода на Python, изменение полей результата AppConfig() приведёт к изменениям в поле синглтона так, как будто это происходит в C++! Выполнив в командной строке Python следующий код:
Мы получим вывод:
Наверное уже все успели заметить, что я люблю перегрузить метод-другой в примере, чтобы объявление получилось как можно более зубодробительным. Сейчас мы для удобства использования класса Single в питоне добавим свойства для чтения счётчика и для получения и задания параметров конфигурации, благо все методы для этого уже есть.
Метод Single::CurrentID обернут свойством current_id на раз-два, зато смотрите какая «красивая» обёртка для двух перегрузок Single::AppConfig, соответственно get- и set-методов свойства app_config. Причём обратите внимание, для get-метода нам пришлось использовать специальную функцию make_function для того, чтобы навесить политику возвращаемого значения return_value_policy<reference_existing_object>.
Будьте очень внимательны, вы не можете использовать функцию make_getter для методов, она используется только для полей классов C++, для методов нужно использовать функции как есть. Если вам требуется задать в одном из методов политику возвращаемого значения для одного из методов свойства, нужно использовать make_function. Вспомогательного дополнительного аргумента для return_value_policy как в .def у вас уже нет, поэтому приходится передавать одним аргументом сразу же и функцию, и политику возвращаемого значения.
Итак, мы уже разобрались, как не давать GC питона удалять возвращаемый по ссылке объект. Однако иногда требуется передать в питон на хранение новый объект. GC корректно удалит объект как только умрётв мучениях последняя переменная, ссылающаяся на ваш результат. Для этого есть политика return_value_policy<manage_new_object>.
Давайте заведём метод, клонирующий параметры конфигурации в новый объект. Добавим во wrap.h объявление:
И во wrap.cpp добавляем её реализацию:
В обёртке класса Single соответственно появится новый метод с политикой manage_new_object:
Для проверки того, что Config действительно удаляется когда надо, объявим деструктор в совсем уже не C-style структуре Config. В деструкторе просто выводим в STDOUT через std::cout поля удаляемого экземпляра Config:
Пробуем!
В тестовом скрипте на Python 3.x клонируем конфиги, поизменяем их по-всякому и сбросим все ссылки на созданный через CloneAppConfig() объект:
Деструкторы вызываются ровно тогда когда это и ожидается, когда на объект пропадает последняя ссылка.
Вот что должно вывестись на экран:
В качестве домашнего задания, попробуйте ещё добавить в обёртку Config метод __del__ — аналог деструктора в Python, увидите насколько непохоже ведут себя обёртки и объекты на которые они ссылаются.
Итак, мы познакомились на практике с двумя новыми политиками возвращаемого значения по ссылке: reference_existing_object и manage_new_object. То есть научились использовать объект-обёртку возвращаемого значения как ссылку на существующий C++ объект, а также передавать на попечение GC Python новые создаваемые в C++ объекты.
Разобрались вкратце как действовать в случае если на дефолтные конструкторы класса в C++ наложены ограничения. Это актуально не только в случае синглтона или абстрактного класса, но также и для множества специфичных классов, примеры которых наверняка сейчас у вас перед глазами.
В третьей части нас ждёт несложная обёртка enum, мы напишем свой конвертер для массива байт из C++ в Python и обратно, а также научимся использовать наследование C++ классов на уровне их обёрток.
Далее нас ждёт волшебный мир конвертации исключений из C++ в Python и обратно.
Что будет после пока загадывать не буду, тема раскручивается как клубок: такой маленький и компактный, пока его разматывать не начинаешь…
Ссылку на проект 2-й части можно найти здесь. Проект MSVS v11, настроен на сборку с Python 3.3 x64.
Документация Boost.Python
Конструктор обёртки класса boost::python::class_
Политики возвращаемых по ссылке значений в Boost.Python
Начало работы с Boost для Windows
Начало работы с Boost для *nix
Тонкости сборки Boost.Python
Продолжаем мучить Boost.Python. В этот раз настала очередь класса, который нельзя ни создать, ни скопировать.
Обернём почти обычную сишную структуру с необычным конструктором.
И поработаем с возвращением ссылки на поле объекта C++, так чтобы сборщик мусора Python его не удалил ненароком. Ну и наоборот, сделаем альтернативный вариант, чтобы Python прибрал мусор после удаления того, что ему отдали на хранение.
Поехали…
Оглавление
- Объединяя C++ и Python. Тонкости Boost.Python. Часть первая
- Объединяя C++ и Python. Тонкости Boost.Python. Часть вторая
- Конвертация типов в Boost.Python. Делаем преобразование между привычными типами C++ и Python
- Путешествие исключений между C++ и Python или «Туда и обратно»
Подготавливаем проект
Нам для наших целей вполне хватит дополнить проект example оставшийся с предыдущей части.
Давайте добавим в него ещё пару файлов для работы с классом синглтона:
single.h
single.cpp
И вынесем объявления вспомогательных функций для обёртки в Python в отдельный файл:
wrap.h
От прежнего проекта должен был остаться файл, который мы активно будем менять:
wrap.cpp
И замечательные файлы с чудо-классом, который так нам помог в первой части, они останутся как есть:
some.h
some.cpp
Оборачиваем простую структуру
Начнём с того, что заведём в single.h небольшую C-style структуру, просто с описанием полей.
Давайте для интереса это будет не просто структура, а некий загадочный тип описания конфигурации:
struct Config
{
double coef;
string path;
int max_size;
Config( double def_coef, string const& def_path, int def_max_size );
};
Сделать обёртку для такой структуры не составит никакого труда, нужно лишь специально описать конструктор с параметрами с помощью шаблона конструктора boost::python::init<...>(...) параметра шаблона обёртки boost::python::class_:
class_<Config>( "Config", init<double,string,int>( args( "coef", "path", "max_size" ) )
.add_property( "coef", make_getter( &Config::coef ), make_setter( &Config::coef ) )
.add_property( "path", make_getter( &Config::path ), make_setter( &Config::path ) )
.add_property( "max_size", make_getter( &Config::max_size ), make_setter( &Config::max_size ) )
;
Как видите, здесь даже не приходится применять return_value_policy<copy_const_reference> для строкового поля. Просто потому, что поле здесь берётся по сути по значению, а стало быть автоматически преобразуется в стандартную строку str языка Python.
Функции make_setter делают ещё очень полезную работу по проверке типа входящего значения, попробуйте например присвоить в питоне полю coef значение строкового типа или задать max_size значением типа float, получите исключение.
Поля сишной структуры Config по сути превращаются в свойства объекта полноценного питоновского класса Config. Ну почти полноценного… Давайте по аналогии с классом Some из прошлой главы добавим в обёртку методы __str__ и __repr__, а заодно добавим свойство as_dict для преобразования полей структуры в стандартный dict питона и обратно.
Объявление новых функций, так же как и старых, перенесём в наш новый файл wrap.h:
#pragma once
#include <boost/python.hpp>
#include "some.h"
#include "single.h"
using namespace boost::python;
string Some_Str( Some const& );
string Some_Repr( Some const& );
dict Some_ToDict( Some const& );
void Some_FromDict( Some&, dict const& );
string Config_Str( Config const& );
string Config_Repr( Config const& );
dict Config_ToDict( Config const& );
void Config_FromDict( Config&, dict const& );
В файле wrap.cpp не останется ничего лишнего и сразу же будет объявление модуля example, что явно прибавит читабельности.
В конце wrap.cpp напишем реализацию наших новых функций, по аналогии с тем, как мы их писали в первой части:
string Config_Str( Config const& config )
{
stringstream output;
output << "{ coef: " << config.coef << ", path: '" << config.path << "', max_size: " << config.max_size << " }";
return output.str();
}
string Config_Repr( Config const& config )
{
return "Config: " + Config_Str( config );
}
dict Config_ToDict( Config const& config )
{
dict res;
res["coef"] = config.coef;
res["path"] = config.path;
res["max_size"] = config.max_size;
return res;
}
void Config_FromDict( Config& config, dict const& src )
{
if( src.has_key( "coef" ) ) config.coef = extract<double>( src["coef"] );
if( src.has_key( "path" ) ) config.path = extract<string>( src["path"] );
if( src.has_key( "max_size" ) ) config.max_size = extract<int>( src["max_size"] );
}
Это я конечно уже с жиру бешусь, но назовём это повторением пройденного.
В обёртку структуры разумеется добавляем новые объявления:
class_<Config>( "Config", init<double,string,int>( args( "coef", "path", "max_size" ) ) )
.add_property( "coef", make_getter( &Config::coef ), make_setter( &Config::coef ) )
.add_property( "path", make_getter( &Config::path ), make_setter( &Config::path ) )
.add_property( "max_size", make_getter( &Config::max_size ), make_setter( &Config::max_size ) )
.def( "__str__", Config_Str )
.def( "__repr__", Config_Repr )
.add_property( "as_dict", Config_ToDict, Config_FromDict )
;
Со структурой ничего сложного, из неё получился замечательный питоновский класс, зеркально повторяющий свойства структуры Config в C++ и при этом класс вполне питонист. Единственная проблема с данным классом будет в том, что в конструктор при создании надо будет что-нибудь указать.
Для заполнения параметров конфигурации и доступа к ним давайте заведём синглтон, заодно снабдим его «полезным» счётчиком.
Обёртка класса без возможности создания и копирования
Итак синглтон. Пусть в нём будут содержаться вышеупомянутые параметры конфигурации текущего приложения и некий волшебный счётчик для получения текущего идентификатора.
class Single
{
public:
static int CurrentID();
static Config& AppConfig();
static void AppConfig( Config const& );
private:
int mCurrentID;
Config mAppConfig;
Single();
Single( Single const& );
static Single& Instance();
int ThisCurrentID();
Config& ThisAppConfig();
void ThisAppConfig( Config const& );
};
Как вы наверное успели заметить, я не очень-то люблю вытаскивать в секцию public бесполезный метод Instance() и предпочитаю работать с функционалом синглтона как с набором статических методов. От этого синглтон не перестаёт быть синглтоном, а пользователь класса скажет вам спасибо, за то, что спрятали вызов Instance() в реализацию.
Вот собственно и она, реализация в файле single.cpp:
#include "single.h"
#include <boost/thread.hpp>
using boost::mutex;
using boost::unique_lock;
const double CONFIG_DEFAULT_COEF = 2.5;
const int CONFIG_DEFAULT_MAX_SIZE = 0x1000;
const string CONFIG_DEFAULT_PATH = ".";
int Single::CurrentID()
{
return Instance().ThisCurrentID();
}
Config& Single::AppConfig()
{
return Instance().ThisAppConfig();
}
void Single::AppConfig( Config const& config )
{
Instance().ThisAppConfig( config );
}
Single::Single()
: mCurrentID( 0 )
{
mAppConfig.coef = CONFIG_DEFAULT_COEF;
mAppConfig.max_size = CONFIG_DEFAULT_MAX_SIZE;
mAppConfig.path = CONFIG_DEFAULT_PATH;
}
Single& Single::Instance()
{
static mutex single_mutex;
unique_lock<mutex> single_lock( single_mutex );
static Single instance;
return instance;
}
int Single::ThisCurrentID()
{
static mutex id_mutex;
unique_lock<mutex> id_lock( id_mutex );
return ++mCurrentID;
}
Config& Single::ThisAppConfig()
{
return mAppConfig;
}
void Single::ThisAppConfig( Config const& config )
{
mAppConfig = config;
}
Всего три статичных метода, обёртка не должна получиться сложной, если не учитывать одно но… хотя нет, не совсем одно:
1. Нельзя создавать экземпляр класса
2. Нельзя копировать экземпляр класса
3. Мы ещё не оборачивали статические методы
class_<Single, noncopyable>( "Single", no_init )
.def( "CurrentID", &Single::CurrentID )
.staticmethod( "CurrentID" )
.def( "AppConfig", static_cast< Config& (*)() >( &Single::AppConfig ), return_value_policy<reference_existing_object>() )
.def( "AppConfig", static_cast< void (*)( Config const& ) >( &Single::AppConfig ) )
.staticmethod( "AppConfig" )
;
Как видите все сложности связанные с 1-м и 2-м пунктам сводятся к указанию параметра шаблона boost::noncopyable и передаче параметра boost::python::no_init конструктору шаблонного класса boost::python::class_.
Если требуется, чтобы класс поддерживал копирование или содержал конструктор по умолчанию, можете стереть соответствующий глушитель генератора свойств класса-обёртки.
Вообще говоря конструктор по умолчанию можно объявить ниже в .def( init<>() ), некоторые так и делают, для единообразия с другими конструкторами с параметрами, описываемыми отдельно, также передавая no_init в конструктор шаблона обёртки. Есть также вариант с заменой конструктора по-умолчанию на конструктор с параметрами прямо при объявлении класса-обёртки, как это мы уже сделали для структуры Config.
С третьим пунктом вообще всё просто, объявлением того, что метод статичный занимается .staticmethod() после объявления через .def() обёртки всех перегрузок данного метода.
В общем-то остальное уже не вызывает вопросов и знакомо нам по первой части, кроме одной забавной мелочи — политики возвращаемого по ссылке значения return_value_policy<reference_existing_object>, о ней далее.
Политика «не бейте меня, я — переводчик»
Самую большую сложность в обёртке методов нашего синглтона вызвало возвращение ссылки на объект из метода
static Config& AppConfig();
Просто для того, чтобы Garbage Collector (GC) интерпретатора Python не удалил содержимое поля класса возвращённого по ссылке из метода, мы используем return_value_policy<reference_existing_object>.
Магия Boost.Python настолько сурова, что при выполнении кода на Python, изменение полей результата AppConfig() приведёт к изменениям в поле синглтона так, как будто это происходит в C++! Выполнив в командной строке Python следующий код:
from example import *
Single.AppConfig().coef = 123.45
Single.AppConfig()
Мы получим вывод:
Config: { coef: 123.45, path: '.', max_size: 4096 }
Добавляем свойство с политикой для get-метода
Наверное уже все успели заметить, что я люблю перегрузить метод-другой в примере, чтобы объявление получилось как можно более зубодробительным. Сейчас мы для удобства использования класса Single в питоне добавим свойства для чтения счётчика и для получения и задания параметров конфигурации, благо все методы для этого уже есть.
.add_static_property( "current_id", &Single::CurrentID )
.add_static_property( "app_config", make_function( static_cast< Config& (*)() >( &Single::AppConfig ), return_value_policy<reference_existing_object>() ), static_cast< void (*)( Config const& ) >( &Single::AppConfig ) )
Метод Single::CurrentID обернут свойством current_id на раз-два, зато смотрите какая «красивая» обёртка для двух перегрузок Single::AppConfig, соответственно get- и set-методов свойства app_config. Причём обратите внимание, для get-метода нам пришлось использовать специальную функцию make_function для того, чтобы навесить политику возвращаемого значения return_value_policy<reference_existing_object>.
Будьте очень внимательны, вы не можете использовать функцию make_getter для методов, она используется только для полей классов C++, для методов нужно использовать функции как есть. Если вам требуется задать в одном из методов политику возвращаемого значения для одного из методов свойства, нужно использовать make_function. Вспомогательного дополнительного аргумента для return_value_policy как в .def у вас уже нет, поэтому приходится передавать одним аргументом сразу же и функцию, и политику возвращаемого значения.
Политика «вот новый объект — удали его»
Итак, мы уже разобрались, как не давать GC питона удалять возвращаемый по ссылке объект. Однако иногда требуется передать в питон на хранение новый объект. GC корректно удалит объект как только умрёт
Давайте заведём метод, клонирующий параметры конфигурации в новый объект. Добавим во wrap.h объявление:
Config* Single_CloneAppConfig();
И во wrap.cpp добавляем её реализацию:
Config* Single_CloneAppConfig()
{
return new Config( Single::AppConfig() );
}
В обёртке класса Single соответственно появится новый метод с политикой manage_new_object:
.def( "CloneAppConfig", Single_CloneAppConfig, return_value_policy<manage_new_object>() )
Для проверки того, что Config действительно удаляется когда надо, объявим деструктор в совсем уже не C-style структуре Config. В деструкторе просто выводим в STDOUT через std::cout поля удаляемого экземпляра Config:
Config::~Config()
{
cout << "Config destructor of Config: { coef: " << coef
<< ", path: '" << path << "', max_size: " << max_size << " }" << endl;
}
Пробуем!
В тестовом скрипте на Python 3.x клонируем конфиги, поизменяем их по-всякому и сбросим все ссылки на созданный через CloneAppConfig() объект:
from example import *
c = Single.CloneAppConfig()
c.coef = 11.11; c.path = 'cloned'; c.max_size = 111111
print( "c.coef = 12.34; c.path = 'cloned'; c.max_size = 100500" )
print( "c:", c ); print( "Single.AppConfig():", Single.AppConfig() )
print( "c = Single.CloneAppConfig()" ); c = Single.CloneAppConfig()
c.coef = 22.22; c.path = 'another'; c.max_size = 222222
print( "c.coef = 22.22; c.path = 'another'; c.max_size = 222222" )
print( "c:", c ); print( "Single.app_config:", Single.app_config )
print( "c = None" ); c = None
print( "Single.app_config:", Single.app_config )
Деструкторы вызываются ровно тогда когда это и ожидается, когда на объект пропадает последняя ссылка.
Вот что должно вывестись на экран:
c.coef = 12.34; c.path = 'cloned'; c.max_size = 100500
c: { coef: 11.11, path: 'cloned', max_size: 111111 }
Single.AppConfig(): { coef: 2.5, path: '.', max_size: 4096 }
c = Single.CloneAppConfig()
Config::~Config() destructor of object: { coef: 11.11, path: 'cloned', max_size: 111111 }
c.coef = 22.22; c.path = 'another'; c.max_size = 222222
c: { coef: 22.22, path: 'another', max_size: 222222 }
Single.app_config: { coef: 2.5, path: '.', max_size: 4096 }
c = None
Config::~Config() destructor of object: { coef: 22.22, path: 'another', max_size: 222222 }
Single.app_config: { coef: 2.5, path: '.', max_size: 4096 }
Config::~Config() destructor of object: { coef: 2.5, path: '.', max_size: 4096 }
В качестве домашнего задания, попробуйте ещё добавить в обёртку Config метод __del__ — аналог деструктора в Python, увидите насколько непохоже ведут себя обёртки и объекты на которые они ссылаются.
В заключении второй части
Итак, мы познакомились на практике с двумя новыми политиками возвращаемого значения по ссылке: reference_existing_object и manage_new_object. То есть научились использовать объект-обёртку возвращаемого значения как ссылку на существующий C++ объект, а также передавать на попечение GC Python новые создаваемые в C++ объекты.
Разобрались вкратце как действовать в случае если на дефолтные конструкторы класса в C++ наложены ограничения. Это актуально не только в случае синглтона или абстрактного класса, но также и для множества специфичных классов, примеры которых наверняка сейчас у вас перед глазами.
В третьей части нас ждёт несложная обёртка enum, мы напишем свой конвертер для массива байт из C++ в Python и обратно, а также научимся использовать наследование C++ классов на уровне их обёрток.
Далее нас ждёт волшебный мир конвертации исключений из C++ в Python и обратно.
Что будет после пока загадывать не буду, тема раскручивается как клубок: такой маленький и компактный, пока его разматывать не начинаешь…
Ссылку на проект 2-й части можно найти здесь. Проект MSVS v11, настроен на сборку с Python 3.3 x64.
Полезные ссылки
Документация Boost.Python
Конструктор обёртки класса boost::python::class_
Политики возвращаемых по ссылке значений в Boost.Python
Начало работы с Boost для Windows
Начало работы с Boost для *nix
Тонкости сборки Boost.Python