Vsebina
Ta modul uporablja:
- https://github.com/leskovec/pyC_part.I
V modulu 03 smo večinoma vezali funkcije. Tukaj naredimo naslednji večji korak: zvežemo C++ razred.
Učni repozitorij implementira majhen C++ razred VectorInt, ki cela števila hrani v std::vector<int>,
in ga v Python izpostavi kot razred PyVectorInt.
Zakaj je ta zasnova uporabna za začetnike:
- jasno vidite, kako C++ objekt postane Python objekt
- naučite se Python »dunder« metod (
__len__,__getitem__,__iter__, ...) - spoznate izpostavljanje preobremenjenih metod, enumov in celo povratnega klica
Osrednja tema ostaja enaka: tanek vezavni sloj mapira C++ -> Python.
Le da je zemljevid zdaj bogatejši kot en sam klic m.def(...).
C++ razred: vektor, skrit za objektom
Deklaracija razreda (iz vector_int.h) je kratka, a poučna:
#pragma once
#include <vector>
#include <string>
enum Access { READONLY, READWRITE };
class VectorInt {
using iterator = typename std::vector<int>::iterator;
public:
VectorInt();
VectorInt(const size_t n);
iterator begin();
iterator end();
void push_back(const int val);
void push_back(const int val, const size_t n);
int get(const size_t idx) const;
size_t size() const;
bool is_empty() const;
void clear();
std::string to_string() const;
private:
std::vector<int> _vec;
public:
Access _access;
};
Nekaj stvari je dobro opaziti takoj:
VectorIntje običajen C++ razred. V njem ni nič »pythonovskega«.- Dejanski podatki so v
_vec, torej zasebnemstd::vector<int>. - Razred ima majhno »nastavitveno stikalo«
_accesstipaAccess(enum). - Razred podpira iteracijo (
begin(),end()), kar bomo potrebovali pri__iter__na Python strani.
Če si iz tega modula zapomnite eno idejo, naj bo to:
Ne vežemo
std::vector<int>neposredno. Vezan je C++ razred, ki znotraj uporabljastd::vector<int>.
To je v realnih projektih zelo pogost vzorec: podrobnosti implementacije skrijete za stabilnim vmesnikom.
Kaj C++ metode dejansko počnejo
Implementacija (vector_int.cpp) je namenoma enostavna.
Konstruktorji
VectorInt::VectorInt() : _vec(), _access(Access::READWRITE) { }
VectorInt::VectorInt(const size_t n) : _vec(n), _access(Access::READWRITE) { }
- Privzeti konstruktor: začnemo s praznim vektorjem.
- Konstruktor z velikostjo: vektor rezervira
nprivzeto inicializiranih celih števil. - V obeh primerih je privzeta politika dostopa
READWRITE.
Push back
void VectorInt::push_back(const int val) {
_vec.push_back(val);
}
void VectorInt::push_back(const int val, const size_t n) {
for (size_t i = 0; i < n; ++i) {
_vec.push_back(val);
}
}
To je odličen primer, zakaj so razredi didaktično uporabni:
- v Pythonu lahko izpostavite naravno ime metode
push_back - hkrati pa morate obravnavati dejstvo, da C++ podpira preobremenitve (isto ime, različni podpisi)
To bomo rešili v vezavni plasti.
Dostop po indeksu in velikost
int VectorInt::get(const size_t idx) const {
return _vec[idx];
}
size_t VectorInt::size() const {
return _vec.size();
}
Ti metodi bomo uporabili za Python __getitem__ in __len__.
Nizovna predstavitev
std::string VectorInt::to_string() const {
std::stringstream ss;
ss << "[";
for (size_t i = 0; i < size() - 1; ++i) {
ss << get(i) << ", ";
}
ss << get(size() - 1) << "]";
return ss.str();
}
Funkcija zgradi berljiv niz, npr. [1, 2, 3].
V Python terminologiji ga bomo uporabili kot __repr__ (za razhroščevanje) in __str__ (za izpis).
Vezavna datoteka: kako C++ razred postane Python razred
Vezavna koda je v module.cpp. To je »mostna« datoteka.
Glave: zakaj vključimo ravno te
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/functional.h>
#include "vector_int.h"
namespace py = pybind11;
pybind11/pybind11.h: osnovni vezavni APIpybind11/stl.h: pretvorbe tipov za STL tipe (seznami ↔ vektorji itd.)pybind11/functional.h: potrebna glava, kadar vežetestd::function(callback)
Tudi če callbackov še ne uporabljate, je koristno vedeti, da je functional.h »opt-in« glava
za klice Python↔C++.
Vezava razreda z py::class_
Osnovna oblika je:
py::class_<VectorInt>(m, "PyVectorInt", py::dynamic_attr())
To lahko berete takole:
- »Izpostavi C++ tip
VectorInt... - ... v modulu
m... - ... pod Python imenom
PyVectorInt.«
Dodatni py::dynamic_attr() izboljša uporabnost na Python strani:
omogoča, da instanci med izvajanjem dodate nove atribute (kot pri mnogih čistih Python razredih).
Primer v Pythonu:
v = vecint.PyVectorInt()
v.label = "trial run 7" # dinamično dodan atribut
Če dynamic_attr izpustite, takšna dodelitev lahko odpove, odvisno od načina vezave razreda.
Naj se razred obnaša pythonovsko z dunder metodami
Konstruktorji -> __init__
.def(py::init<>()) // PyVectorInt()
.def(py::init<const size_t>()) // PyVectorInt(n)
V Pythonu to omogoča dva načina konstrukcije:
- prazen vektor
- vektor vnaprej določene velikosti
Izpis: __repr__ in __str__
.def("__repr__", &VectorInt::to_string)
.def("__str__", &VectorInt::to_string)
V Pythonu:
print(v)kliče__str__- interaktivni poziv uporablja
__repr__
V učnem primeru ju pustimo enaka, kar je povsem v redu.
Indeksiranje: v[i]
.def("__getitem__", &VectorInt::get)
V Pythonu lahko zdaj pišete:
v[0]
Opomba: C++ get() trenutno uporablja _vec[idx] in ne preverja meja.
Za produkcijsko kodo bi navadno dodali preverjanje in vrgli izjemo ob neveljavnem indeksu.
Dolžina: len(v)
.def("__len__", &VectorInt::size)
V Pythonu:
len(v)
Iteracija: for x in v: ...
Tu postane zanimivo:
.def("__iter__", [](VectorInt& vec) {
return py::make_iterator(vec.begin(), vec.end());
}, py::keep_alive<0,1>())
Kaj se dogaja?
make_iterator(begin, end)ustvari Python iterator, ki hodi po C++ iteratorjih.keep_alive<0,1>()je pomemben: pybind11 pove, naj C++ vsebnik (vec) ostane živ toliko časa, kot obstaja Python iterator.
Brez keep_alive je precej lažje nehote končati z visečim iteratorjem, če izvorni objekt
gre iz dosega.
Izpostavljanje običajnih metod (tudi preobremenjenih)
Preobremenjeni push_back
C++ podpira preobremenitve, Python pa jih ne razrešuje po tipih na isti način. Zato v vezavi pogosto eksplicitno izberemo pravi podpis:
.def("push_back",
static_cast<void (VectorInt::*)(const int)>(&VectorInt::push_back),
"Insert one element", py::arg("elem"))
.def("push_back",
static_cast<void (VectorInt::*)(const int, const size_t)>(&VectorInt::push_back),
"Insert elem n times", py::arg("elem"), py::arg("n"))
static_cast<...> je ključni del: odstrani dvoumnost in pove, katero preobremenitev vežete.
V Pythonu sta obe pod istim imenom:
v.push_back(7)
v.push_back(7, 10) # vstavi 10 kopij
Ostale metode
To so preproste vezave:
.def("is_empty", &VectorInt::is_empty)
.def("clear", &VectorInt::clear)
.def("size", &VectorInt::size)
Repozitorij veže tudi length kot lastnost samo za branje:
.def_property_readonly("length", &VectorInt::size)
V Pythonu deluje oboje:
v.size()
v.length
Vezava javnega podatkovnega člana: .def_readwrite
Razred ima javni član _access. Vezava uporablja:
.def_readwrite("access", &VectorInt::_access)
S tem dobite Python atribut v.access, ki je berljiv in zapisljiv.
Za poučevanje je to odličen primer: izpostavite lahko tudi podatkovne člane, ne samo metod.
V resničnih projektih boste pogosto raje uporabili def_property, da lahko ob zapisu izvajate validacijo,
a def_readwrite je dober prvi korak.
Vezava enuma: Access
Repozitorij izpostavi C++ enum v Python:
py::enum_<Access>(m, "Access")
.value("READONLY", Access::READONLY)
.value("READWRITE", Access::READWRITE);
Tako lahko uporabnik v Pythonu zapiše:
v.access = vecint.Access.READONLY
To je bistveno bolje kot uvajanje »čarobnih celih števil«.
Primer callbacka: C++ kliče nazaj v Python
Vezava vključuje tudi metodo, ki sprejme klicljiv objekt:
.def("doForAllElements",
[](VectorInt& vec, std::function<void(int)>& f) {
for (int elem : vec) {
f(elem);
}
});
To je pomemben konceptualni korak:
- Python poda funkcijo (ali lambda) v C++
- C++ iterira po internem vektorju
- C++ za vsak element pokliče Python funkcijo
V Pythonu je to videti takole:
def printer(x):
print("element:", x)
v.doForAllElements(printer)
To ni najhitrejši način numeričnega računanja (večkratni klici v Python so dragi), je pa didaktično zelo uporaben za razlago:
- kako
std::functionna C++ strani predstavlja klicljiv objekt - zakaj ste vključili
pybind11/functional.h - kako enostavno je sestaviti C++ vsebnike in Python vedenje
Dobra učna poanta o zmogljivosti:
Če Python kličete enkrat na element, ceno prehoda plačate velikokrat. Za hitrost so boljša jedra, ki obdelajo več podatkov v enem klicu, callbacke pa razumite kot orodje.
Ključni povzetek
Ta modul je več kot »vezava vektorja«. Pokaže vzorce, ki jih boste uporabljali pri realnih razredih:
- konstruktorji ->
py::init - Python vedenje prek dunder metod (
__len__,__getitem__,__iter__, ...) - razreševanje preobremenitev s
static_cast - lastnosti in javni člani (
def_property_readonly,def_readwrite) - enumi (
py::enum_) - callbacki (
std::function+pybind11/functional.h)
Ko znate prebrati in napisati takšno vezavo, ste pripravljeni izpostaviti kompleksnejše znanstvene objekte z jasnim API-jem, malo prehodi med jeziki in predvidljivim pomnilniškim obnašanjem.