Как стать автором
Обновить

Объединяя C++ и Python. Тонкости Boost.Python. Часть вторая

Время на прочтение10 мин
Количество просмотров26K
Данная статья является продолжением первой части.
Продолжаем мучить 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"

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
Теги:
Хабы:
Всего голосов 40: ↑39 и ↓1+38
Комментарии3

Публикации

Истории

Работа

Программист C++
133 вакансии
Python разработчик
137 вакансий
QT разработчик
8 вакансий
Data Scientist
61 вакансия

Ближайшие события