Moduly pro Python (2)

25. 11. 2003
Doba čtení: 5 minut

Sdílet

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:

bitcoin_skoleni

>>> 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.