Narzędzia użytkownika

Narzędzia witryny


boost_python

Biblioteka Boost Python

Kamil Leszczuk, G1SST

Wstęp

Biblioteka Boost Python umożliwia korzystanie z kodu napisanego w C++ z poziomu skryptów Pythona.

Do dyspozycji mamy nie tylko proste funkcje, ale także całe klasy (i ich hierarchie) czy metody klas - wraz z tymi wirtualnymi. Wykorzystać można także przeciążone operatory czy pola tylko do odczytu.
Biblioteka Boost Python jest wygodnym narzędziem także ze względu na prostotę swojego interfejsu - zdecydowana większość kodu C++ ma duże szanse współpracować z tą bilblioteką out-of-box, bez niespodzianek i problemów.

Wymagania

Aby rozpocząć pracę z bibliotekami boost::python należy posiadać w systemie (oprócz kompilatora oczywiście):

  • Interpreter Pythona
  • Pakiet devel Pythona - pliki nagłówkowe (jeśli nie zostały dostarczone wraz z instalacją Pythona)
  • Biblioteki boost

Pierwsze lody

Dobrze, spróbujmy więc opanować podstawy. Weźmy na przykład ten kod C++:

#include <boost/python.hpp>
 
using namespace boost::python;
 
int potega ( int a ) {
        return a*a;
}
 
BOOST_PYTHON_MODULE(p1) {
        .def("skomplikowany_algorytm", &potega);
}

Widzimy jednoparametrową funkcję potega. Nie różni się ona niczym innych funkcji napisanych w C/C++. Ciekawsze są linijki znajdujące się bezpośrednio pod nią - dajemy tam znać, że tworzymy moduł Pythona o nazwie p1, który udostępni jedną funkcję o nazwie skomplikowany_algorytm. Zwróćmy uwagę, że nazwa funkcji udostępniona przez moduł Pythona nie musi pokrywać się z nazwą funkcji z C++.

Czas na kompilację - należy zapewnić kompilatorowi dostęp do odpowiednich nagłówków (bibliotek boost i Pythona), a także linkować z biblioteką boost python, np.:

g++ -Wall -shared -I/usr/include/boost/ -I/usr/include/python2.5/ /usr/lib/libboost_python-mt.so p1.cpp -o p1.so

Jako wynik powinniśmy uzyskać plik p1.so, który jest właśnie naszym modułem Pythona. Stwórzmy więc skrypt który z tego modułu skorzysta:

Uwaga: moduł powinien znaleźć się w tym samym katalogu co skrypt, aby interpreter mógł go bezproblemowo znaleźć ( katalog bierzący '.' znajduje się w domyślnej ścieżce poszukiwania Pythona).

#!/usr/bin/python
import p1
 
print p1.skomplikowany_algorytm(7)

Wykonajmy nasz skrypt (pamiętając aby ewentualnie zmienić ścieżkę do interpretatora Pythona, jeżeli w systemie jest inna), np.:

$ ./skrypt.py
49

Voila! Nasz niesamowicie skomplikowany algorytm zadziałał.

Coś ciekawszego

Teraz, kiedy już znamy podstawy, spróbujmy użyć kodu C++ nieco bardziej podobnego do tego spotykanego na codzień. Weźmy taką hierarchię klas:

#include <iostream>
 
class Costam_0 {
        public:
                virtual void metoda ( std::string suffix ) = 0;
                virtual ~Costam_0 () {}
};
 
class Costam_A : public Costam_0 {
        public:
                Costam_A() {}
                Costam_A( std::string nazwa ) : nazwa_(nazwa) {}
                virtual void metoda ( std::string suffix ) { nazwa_ += " B "+suffix; }
                std::string daj_nazwe () const { return nazwa_; }
                void ustaw_nazwe ( const std::string nazwa ) { nazwa_ = nazwa; }
        protected:
                std::string nazwa_;
};
 
class Costam_B : public Costam_A {
        public:
                Costam_B() {}
                Costam_B( std::string nazwa ) : Costam_A(nazwa) {}
                virtual void metoda ( std::string suffix ) { nazwa_ += " B " + suffix; }
                Costam_B& operator+ ( std::string x ) { nazwa_ += " ++ " + x; return *this; }
 
                std::string x_;
};

Widzimy trzy klasy. Abstrakcyjną Costam_0, Costam_A po niej dziedziczącą oraz Costam_B dziedziczącą po Costam_A. Jak taką hierarchię udostępnić jako moduł Pythona?

Aby udostępnić klasę Costam_0 należy dopisać kod tego typu:

class Costam_0Wrap : public Costam_0, public wrapper<Costam_0>
{
        virtual void metoda ( std::string suffix ) {
                this->get_override("metoda")();
                // Dla kompilatora MSVC należy zamienić powyższą linijkę na:
                // this->get_override("metoda").ptr();
        }
};
 
/* ..... */
 
BOOST_PYTHON_MODULE(p2) {
        class_<Costam_0Wrap, boost::noncopyable>("Costam0")
                .def("metoda", pure_virtual(&Costam_0::metoda))
        ;
}

Potrzebujemy sppecjalnego wrappera dla Costam_0 ponieważ twórcy biblioteki uznali, że najlepiej jest gdy pisząc moduł Pythona nie musimy modyfikować istniejącego kodu C++.
Wrapper ten nie robi właściwie nic oprócz wołania odpowiedniej metody. Warto zauważyć, że potrzebujemy go tylko dlatego, że klasa Costam_0 posiada czysto wirtualną metodę.
W definicji modułu widzimy, że najpierw deklarujemy chęć udostępnienia klasy - jako nazwę podajemy jednak nazwę klasy wrappera! Klasa ta widoczna będzie w Pythonie pod nazwą Costam0 i będzie posiadała jedną metodę. Następnie definiujemy funkcję metoda zaznaczając, że jest ona czysto wirtualną składową klasy.

Zajmijmy się teraz klasą Costam_A. Dziedziczy ona po Costam_0. Aby ją udostępnić, musimy dopisać poniższy kod w ramach bloku BOOST_PYTHON_MODULE:

        class_<Costam_A, bases<Costam_0>  >("CostamA")
                .def(init<std::string>())
                .add_property("nazwa_prop", &Costam_A::daj_nazwe, &Costam_A::ustaw_nazwe)
                ;

Ten kod właściwie sam siebie tłumaczy. Klasa będzie widoczna pod nazwą CostamA. Posiadała będzie dwa konstruktory - domyślny bezparametrowy oraz z jednym parameterem typu string (druga linia). Konstruktor domyślny jest standardowo udostępniany, dlatego jawnie tego nie zażądaliśmy. Klasa dziedziczy po Costam_0 - mówi o tym fragment bases<Costam_0>.
Dodajemy także składową o nazwie nazwa_prop - mechanizm za nią stojący nie jest właściwie dostępny w C++ - zamiast niego używa się akcesorów. Składowa ta zachowywała się będzie tak jak pole klasy z tą różnicą, że zamiast dokonywać bezpośredniego przypisania do niej wartości, skorzystamy z akcesorów C++ (metody daj_nazwe i ustaw_nazwe).
I w końcu - zwróćmy uwagę na brak definicji metody metoda - jako że nasza klasa dziedziczy po Costam_0, i w tamtej klasie tę metodę udostępniliśmy, teraz już nie musimy tego robić.

Ostatnia z klas udostępniona może być w taki sposób:

        class_<Costam_B, bases<Costam_A> >("CostamB", init<std::string>())
                .def_readonly("x_ro", &Costam_B::x_)
                .def_readwrite("x_rw", &Costam_B::x_)
                .def(self + std::string())
                ;

Ponownie, nie definiujemy jeszcze raz elementów z klas stojących wyżej w hierarchii - zajmujemy się tylko nowymi składowymi. Dodajemy pole x_ro klasy, które będzie przyjmowało wartość zmiennej x_, lecz będzie tylko do odczytu - kod Pythona nie będzie mógł zmienić jego wartości. Z kolei własność x_rw jest tym samym, lecz umożliwia dodatkowo zmianę wartości zmiennej. W tym przykładzie te dwie własności wskazują na tę samą zmienną z C++, lecz równie dobrze mogłyby to być zupełnie różne obiekty - nie ma to znaczenia.

Dodatkowo widzimy jeszcze jedną możliwość biblioteki boost::python - udostępnianie przeciążonych operatorów C++ - tutaj na przykładzie operatora '+' z parametrem typu string.

Klasa ta wprowadza także dodatkową nowość - tym razem nie udostępniamy domyślnego, bezparametrowego konstruktora. Jak uzyskaliśmy ten efekt? Zwróćmy uwagę na pierwszą linijkę - wskazujemy tam, że domyślnie udostępnionym konstruktorem będzie ten pobierający napis (init<std::string>).

Skoro mamy już gotowy drugi moduł, przetestujmy go:

#!/usr/bin/python
import p2
 
# Odziedziczylismy po klasie Costam_B, przeciazajac metode 'metoda'
class CostamC(p2.CostamB):
	def metoda(self, suffix):
		self.nazwa_prop += " C " + suffix 
 
 
# Stworzmy obiekty kazdej z klas:
a = p2.CostamA("AAA")
b = p2.CostamB("BBB")
c = CostamC("CCC")
 
# To nie zadziala! Konstruktor bezparametrowy nie zostal udostepniony
# b_prim = p2.CostamB()
 
# Wolamy metode 'metoda'. Jako ze jest ona wirtualna, 
# powinnismy uzyskac nieco inny efekt za kazdym razem...
a.metoda("aaa");
b.metoda("bbb");
c.metoda("ccc");
 
# ... i tak faktycznie jest:
print a.nazwa_prop
print b.nazwa_prop
print c.nazwa_prop
 
# Przetestujmy takze operator dodawania dla klasy CostamB
# (zadziala takze dla klasy CostamC jako ze dziedziczy po CostamB)
b = b + "PLUS DZIALA" 
print b.nazwa_prop
 
# Zostaly jeszcze pola read-only i read-write dla zmiennej x:
b.x_rw = "Zapisalo sie!" 
print b.x_rw
print b.x_ro
# Ale to nie zadziala: x_ro jest tylko do odczytu!
#b.x_ro = "To sie nie zapisze!" 

Po uruchomieniu naszym oczom ukaże się:

$ ./p2.py
AAA A aaa
BBB B bbb
CCC C ccc
BBB B bbb ++ PLUS DZIALA
Zapisalo sie!
Zapisalo sie!

Widzimy więc, że wszystkie elementy działają poprawnie. Co więcej - po odkomentowaniu niektórych fragmentów - np. próby zapisania do zmiennej tylko do odczytu x_ro interpreter poinformuje nas o błędzie:

$ ./p2.py
AAA A aaa
BBB B bbb
CCC C ccc
BBB B bbb ++ PLUS DZIALA
Zapisalo sie!
Zapisalo sie!
Traceback (most recent call last):
  File "./p2.py", line 39, in <module>
    b.x_ro = "To sie nie zapisze!" 
AttributeError: can't set attribute

Z życia wzięte

Czyli działa - po co jednak aż tak się gimnastykować skoro można napisać te klasy od razu w Pythonie? Powodów jest kilka:

  • Kod który piszemy może być wąskim gardłem aplikacji - warto napisać go w C++ ze względu na wydajność
  • Mamy już kod w C++ - po co przepisywać go od nowa?
  • Aplikację napisać w C++ a GUI w, dużo przyjemniejszym, Pythonie
  • Mamy dostęp do biblioteki C++, ale nie mamy do niej źródeł

W tym ostatnim przypadku jedyne co nam będzie potrzebne to pliki nagłówkowe tej bilioteki i trochę minut czasu. Weźmy klasy z poprzedniego przykładu, rozdzielmy je na plik nagłówkowy i źródłowy.

Plik nagłówkowy wyglądać może tak:

class Costam_0 {
        public:
                virtual void metoda ( std::string suffix ) = 0;
                virtual ~Costam_0 ();
};
 
class Costam_A : public Costam_0 {
        public:
                Costam_A();
                Costam_A( std::string nazwa ); 
                virtual void metoda ( std::string suffix ); 
                std::string daj_nazwe () const;
                void ustaw_nazwe ( const std::string nazwa ); 
        protected:
                std::string nazwa_;
};
 
class Costam_B : public Costam_A {
        public:
                Costam_B();
                Costam_B( std::string nazwa );
                virtual void metoda ( std::string suffix );
                Costam_B& operator+ ( std::string x );
 
                std::string x_;
};

Plik źródłowy skompilujmy do postaci biblioteki współdzielonej:

$ g++ -Wall -shared p2-shared.cpp -o p2-shared.o

Teraz stwórzmy plik p2.cpp z definicją naszego modułu Pythona:

#include "p2-shared.h"
#include <boost/python.hpp>
using namespace boost::python;
 
class Costam_0Wrap : public Costam_0, public wrapper<Costam_0>
{
        virtual void metoda ( std::string suffix ) {
                this->get_override("metoda")();
        }
};
 
 
 
BOOST_PYTHON_MODULE(p2) {
        class_<Costam_0Wrap, boost::noncopyable>("Costam0")
                .def("metoda", pure_virtual(&Costam_0::metoda))
                ;
        class_<Costam_A, bases<Costam_0>  >("CostamA")
                .def(init<std::string>())
                .add_property("nazwa_prop", &Costam_A::daj_nazwe, &Costam_A::ustaw_nazwe)
                ;
        class_<Costam_B, bases<Costam_A> >("CostamB", init<std::string>())
                .def_readonly("x_ro", &Costam_B::x_)
                .def_readwrite("x_rw", &Costam_B::x_)
                .def(self + std::string())
                ;
}

i skompilujmy go nie zapominając o linkowaniu z naszą biblioteką p2-shared.o:

g++ -Wall -shared -I/usr/include/boost/ -I/usr/include/python2.5/ /usr/lib/libboost_python-mt.so p2-shared.o p2.cpp -o p2.so

I gotowe! Możemy teraz uruchomić skrypt z poprzedniego przykładu. A to wszystko bez dostępu do plików źródłowych!
(w rzeczywistej sytuacji plik p2-shared.o byłby biblioteką której źródeł nie mamy - jak widać nawet pomimo tego udało się wystawić ją jako moduł Pythona)

Więcej informacji

boost_python.txt · ostatnio zmienione: 2008/04/16 07:24 przez kamituel