Hlavní navigace

Moduly pro Python (2)

Jan Němec 25. 11. 2003

Python je jazyk s podporou objektově orientovaného programování. V prvním dílu našeho seriálku jsem popsal jednoduchý C modul do Pythonu, který obsahoval jen obyčejné globální funkce. Nyní ukážu, jak zabalit C++ třídu do pythonovského rozhraní. Výsledkem bude opět třída, jen v jiném jazyce.

V úvodních řádkách jsem nastínil docela praktický problém. Někdo před námi napsal knihovnu v C++ a my ji chceme používat z Pythonu. Celkem přirozeně požadujeme, abychom k ní i z Pythonu přistupovali jako ke třídě a public metody si jedna k jedné odpovídaly.

Pozorný a přemýšlivý čtenář už jedno řešení vlastně zná: k C++ třídě si napíše rozhraní ve stylu C, vytvoří modul (přesně podle předchozího dílu) využívající toto rozhraní a přímo v Pythonu (nikoli jeho C API) je obalí objektově orientovanou obálkou. Řešení bude sice fungovat, ale počet mezivrstev, čistota i efektivita návrhu by mohly sloužit jako odstrašující příklad v učebnicích programování. Mnohem lepší je smrsknout celé rozhraní na jediný pythonovský modul v C++.

Dejme tomu, že třída, kterou chceme z Pythonu využívat, vypadá asi takhle:

class Vypisovac{
  public:
  // přiřadí data do private položek,
  //řetězec naalokuje a zkopíruje
    Vypisovac(int pocet = 1, char *str = "Ahoj světe !");
  // vypíše pocet * str
    void vypis();
  // uvolní paměť řetězce
    ~Vypisovac();
  private:
    int pocet;
    char *str;
};

Její praktické využití je sice diskutabilní, ale pro nás je důležitější, že má aspoň základní atributy tříd, jak je známe z C++, tj. proměnné, konstruktor s volitelnými parametry, metodu a destruktor.

Nyní už k tomu hlavnímu, našemu modulu. Tentokrát jej napíšeme pochopitelně v C++, důvodem ovšem není to, že jde o implementaci pythonovské třídy (objektově orientované programování lze realizovat i bez speciální podpory jazyka – s čistým céčkem bychom si vystačili), musíme ale používat C++ třídu Vypisovac, což by z C nešlo. Zdroják modulu uložíme jako vypisovacmodule.cc.

#include <Python.h>
#include "vypisovac.h"

Hlavičkové soubory asi nikoho nepřekvapily.

typedef struct {
    // Standardní hlavička objektu.
    PyObject_HEAD
    // Instance C++ Vypisovace
    Vypisovac *vypisovac;
} VypisovacObject;

Takhle budou v Pythonu vypadat data jedné instance naší třídy. Makro PyObjectHEAD se rozvine na interní data Pythonu, konkrétně počet referencí na objekt a jeho typ, ale to pro nás teď není důležité. Přímo budeme využívat pouze pointer na C++ Vypisovac.

static PyObject* Vypisovac_Konstruktor(PyObject *self, PyObject *args, PyObject *kwd);
static void Vypisovac_Destruktor(PyObject *self);
static PyObject* Vypisovac_Getattr(PyObject *self,char *name);
static PyObject* Vypisovac_Vypis(PyObject *self, PyObject *args);

Pythonovské třídě Vypisovac musíme napsat konstruktor, destruktor, metodu Vypis a getattr. Připomínám, že getattrpoužije interpret Pythonu, když potřebuje přistoupit k nějakému atributu objektu, například metodě Vypis.

static PyTypeObject VypisovacType = {
    PyObject_HEAD_INIT(NULL)
    0,                       /*ob_size*/
    "Vypisovac",             /*tp_name*/
    sizeof(VypisovacObject), /*tp_basicsize*/
    0,                       /*tp_itemsize*/
    /* methods */
    Vypisovac_Destruktor,    /*tp_dealloc*/
    0,                       /*tp_print*/
    Vypisovac_Getattr,       /*tp_getattr*/
    0,                       /*tp_setattr*/
    0,                       /*tp_compare*/
    0,                       /*tp_repr*/
    0,                       /*tp_as_number*/
    0,                       /*tp_as_sequence*/
    0,                       /*tp_as_mapping*/
    0,                       /*tp_hash */
};

I samotné třídě odpovídá datová struktura (společná pro všechny instance), má spoustu položek, které naštěstí většinou nepotřebujeme a můžeme je beztrestně nastavit na nulu. Zadali jsme pouze jméno třídy, velikost její instance a dále pointery na destruktor a metodu getattr. Teď napíšeme čtyři funkce: konstruktor, destruktor, getattr a metodu vypis. To už téměř umíme, jde spíš o to, jak říci Pythonu, čemu odpovídá jaká C++ funkce. Konstruktor je běžná globální funkce vracející objekt, na destruktor a getattr máme pointery ve struktuřeVypi­sovacType a metodou vypis bude za běhu hledat getattr. Konstruktor vypadá takhle:

static PyObject* Vypisovac_Konstruktor(PyObject *self, PyObject *args, PyObject *kwd)
{
  static char *kwdlist[] = {"pocet", "str", NULL};
  int pocet = 1;
  char *str = "Ahoj světe!";
  VypisovacObject *v;

  // vyparsujeme argumenty
  if(!PyArg_ParseTupleAndKeywords(args, kwd, "|is:konstruktor",
  kwdlist, &pocet, &str))
    return NULL;
  // vytvoříme objekt Pythonu
  v = PyObject_New(VypisovacObject, &VypisovacType);
  // a C++
  v -> vypisovac = new Vypisovac(pocet, str);
  return (PyObject *)v;
}

Konstruktor má volitelné parametry, proto je v Pythonu přirozené připustit i keyword argumenty, to zajistíme voláním PyArg_ParseTu­pleAndKeywords místo PyArg_ParseTuple. Interpret musí znát i jména argumentů, takže PyArg_ParseTu­pleAndKeywords má navíc ještě parametr kwdlist. Text konstruktor za dvojtečkou ve formátovacím parametru se použije při chybovém hlášení, například když uživatel zavolá konstruktor se špatným počtem parametrů. Makro PyObject_New vytvoří nový pythonovský objekt třídyVypisovacType a přetypuje ho na VypisovacObject *.

static void Vypisovac_Destruktor(PyObject *self)
{
  delete ((VypisovacObject *)self) -> vypisovac;
  PyObject_Del(self);
}

Destruktor asi nikoho nepřekvapí. Nejprve smažeme C++ objekt a hned poté jeho pythonovský protějšek. Přejděme ke getattr, v našem případě hledá jen jediný atribut – metodu vypis.

static PyMethodDef MetodyVypisovace[] = {
  {"vypis", (PyCFunction)Vypisovac_Vypis, METH_VARARGS, "komentar"},
  {NULL, NULL}
};

static PyObject *Vypisovac_Getattr(PyObject *self,char *name)
{
  return Py_FindMethod(MetodyVypisovace, self, name);
}

Py_FindMethod projde pole MetodyVypisovace a zkusí najít metodu vypis. V případě neúspěchu (chybné volání z Pythonu) vrátí NULL a vše skončí výjimkou.

static PyObject* Vypisovac_Vypis(PyObject *self, PyObject *args)
{
  // vyparsujeme 0 argumentů
  if(!PyArg_ParseTuple(args, "")) return NULL;
  ((VypisovacObject *)self) -> vypisovac -> vypis();
  Py_INCREF(Py_None);
  return Py_None;
}

Na implementaci metody vypis není nic zajímavého, vše už známe z prvního dílu našeho seriálku. V parametru self máme pointer na VypisovacObject, ovšem je třeba ho přetypovat, neboť prototyp vyžaduje PyObject *.
Zbytek už také známe z minula:

static PyMethodDef MetodyModulu[] = {
  {"Vypisovac", (PyCFunction) Vypisovac_Konstruktor,
    METH_VARARGS | METH_KEYWORDS, "komentar"},
  {NULL, NULL}
};

extern "C" void initvypisovac(void)
{
  Py_InitModule("vypisovac", MetodyModulu);
}

Modul přeložíme stejně jako minule, jen místo gcc zavoláme g++ a musíme přilinkovat vypisovací knihovnu.

Teď už se můžeme pokochat:

>>> import vypisovac
>>> v = vypisovac.Vypisovac(str = "retezec", pocet = 3)
>>> v.vypis()
retezec
retezec
retezec
>>>

A to je vše. Kompletní zdrojáky dnešního příkladu (včetně jednoduchého Makefilu) si můžete stáhnout zde.

Tímto bych náš dvoudílný seriál uzavřel. Sám jsem se s Pythonem seznámil spíše letmo a netroufám si na další pokračování. Ostatně není to ani zapotřebí. Myslím, že absolvent těchto dvou lekcí by již měl bez problémů strávit dokumentaci k C API na stránkách Pythonu.

Našli jste v článku chybu?

1. 12. 2003 21:54

Jan Němec (neregistrovaný)

Jsem rád, že aspoň jednomu čtenáři byl můj článek na něco dobrý :-)
Já osobně v C++ raděj přistupuji ke třídám přes pointery, neboť pak za prvé volám konstruktory a destruktory kdy chci já a ne, kdy to chce překladač (v tom lepším případě norma C++) a za druhé mohu v případě hierarchie použít potomka místo původní třídy, takže se kód dá lépe rozšiřovat. V řadě konkrétních případů tento můj zvyk nemá opodstatnění, to uznávám.


28. 11. 2003 13:02

Zdenek Pavlas (neregistrovaný)

Sem kdysi potřeboval taky balit C++, a byl jsem línej to nastudovat pořádně, podle tohoto návodu to vypadá jako hračka.

Jenom mi není jasnné proč je uvnitř Pythonovského objektu zbytečná indirekce na C++ objekt, když ten má pevnou délku. Bylo by jistě rychlejší a úspornější jej embeddovat, ne?



Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: I církev dnes vyrábí potraviny

I církev dnes vyrábí potraviny

DigiZone.cz: Další dva kanály nabídnou HbbTV

Další dva kanály nabídnou HbbTV

Vitalia.cz: Jak koupit Mikuláše a nenaletět

Jak koupit Mikuláše a nenaletět

Vitalia.cz: Žloutenka v Brně: Nakaženo bylo 400 lidí

Žloutenka v Brně: Nakaženo bylo 400 lidí

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

Vitalia.cz: Chtějí si léčit kvasinky. Lék je jen v Německu

Chtějí si léčit kvasinky. Lék je jen v Německu

Měšec.cz: Zdravotní a sociální pojištění 2017: Připlatíte

Zdravotní a sociální pojištění 2017: Připlatíte

Podnikatel.cz: 1. den EET? Problémy s pokladnami

1. den EET? Problémy s pokladnami

DigiZone.cz: Flix TV má set-top box s HEVC

Flix TV má set-top box s HEVC

Podnikatel.cz: Prodává přes internet. Kdy platí zdravotko?

Prodává přes internet. Kdy platí zdravotko?

Vitalia.cz: Říká amoleta - a myslí palačinka

Říká amoleta - a myslí palačinka

Lupa.cz: UX přestává pro firmy být magie

UX přestává pro firmy být magie

Podnikatel.cz: EET zvládneme, budou horší zákony

EET zvládneme, budou horší zákony

Vitalia.cz: Tesco: Chudá rodina si koupí levné polské kuře

Tesco: Chudá rodina si koupí levné polské kuře

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

Podnikatel.cz: Na poslední chvíli šokuje vyjímkami v EET

Na poslední chvíli šokuje vyjímkami v EET

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

Root.cz: Vypadl Google a rozbilo se toho hodně

Vypadl Google a rozbilo se toho hodně

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí