Продолжаем мучить Boost.Python. В этот раз настала очередь класса, который нельзя ни создать, ни скопировать.
Обернём обычные обычную сишную структуру с необычным конструктором.
И поработаем с возвращением ссылки на поле объекта C++, так чтобы сборщик мусора Python его не удалил ненароком. Ну и наоборот, сделаем альтернативный вариант, чтобы 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" #include "protocol.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 корректно удалит объект как только умрёт в мучениях последняя переменная, ссылающаяся на ваш результат. Для этого есть политика return_value_policy<manage_new_object>.
Давайте заведём метод, клонирующий параметры конфигурации в новый объект. Добавим во 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
ссылка на оригинал статьи http://habrahabr.ru/post/168233/
Добавить комментарий