====== 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 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 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 { 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_("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_ >("CostamA") .def(init()) .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''**. \\ 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_ >("CostamB", init()) .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''). 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 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 using namespace boost::python; class Costam_0Wrap : public Costam_0, public wrapper { virtual void metoda ( std::string suffix ) { this->get_override("metoda")(); } }; BOOST_PYTHON_MODULE(p2) { class_("Costam0") .def("metoda", pure_virtual(&Costam_0::metoda)) ; class_ >("CostamA") .def(init()) .add_property("nazwa_prop", &Costam_A::daj_nazwe, &Costam_A::ustaw_nazwe) ; class_ >("CostamB", init()) .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 ==== * [[http://www.boost.org/libs/python|Witryna biblioteki]] * [[http://wiki.python.org/moin/boost.python|Wiki na stronie pythona]]