A Python programozási nyelv

A Python kiterjesztése C/C++-ból

Miért jó a bővíthetőség?

Két dolog jut eszembe:

A kiterjesztés lehetősége jól jön még azoknak, akik gyors prototipizálásra használják a nyelvet. Ilyenkor a készülő programot először teljesen pythonban írják meg, aztán a problémás rutinokat kicserélik C/C++ kódra, így a fejlesztés is viszonylag gyors és a program futása is :-).

Hogyan bővíthetünk

Az interpretert bővítőmodulokkal tudjuk bővíteni. Bővítőmodulokat C-ben írhatunk. Használhatunk C++-t is a modulok megírásához, de ez esetben minden interpreter álal hívott függvény deklarációját extern "C" {} blokkba kell zárni, illetve ha a python interpretert nem C++-szal szerkesztettük (linkeltük), akkor nem használhatunk olyan globális/statikus objektumokat, amiknek konstruktora van.

A bővítmények két dolgot tehetnek meg, amit python kód nem:

Megjegyzés: A második pont lehetővé teszi, hogy bármely C-ből hívható nyelven megírt rutint pythonból is elérhessünk.

Lehetőség van modul kódból python objektumok lefoglalására és python műveletek meghívására.

Mivel a bővítőmudul implementálásának nyelve C, ezért a modult le kell fordítani mielőtt használni tudnánk egy C fordítóval. A fordítás mikéntje függ a modul tervezett használatának módjától és a rendszer felépítésétől, ahol a modult használni akarjuk majd. A részleteket majd később.

Egy egyszerű példa

A következő példa a Python dokumentációban használt spam modul szégyentelen koppintása.

Haladjunk a cél felől visszafelé: egy olyan modult szeretnénk írni, ami lefuttat egy operációsrendszer-műveletet a C runtime system hívásának segítségével.

A modult a következőképpen szeretnénk használni:

>>> import spam >>> status = spam.system ("ls -la")

Kezdetnek hozzunk létre egy C forrásfájlt, mondjuk spammodule.c néven. Konvenció, hogy a modult implementáló C forrásfájl neve a modul neve (most "spam") legyen szuffixálva a "module" szóval. Ha a modul neve nagyon hosszú, akkor elfogadott, hogy elhagyjuk a "module" szuffixet (pl. mycompalgebra.c mycompalgebramodule.c helyett).

Az első sor legyen:

#include <Python.h>
A Python.h fejléc, tartalmazza az interpreter belső adatstruktúráinak definícióit. Ebben minden publikusan látható szimbólum a Py_ vagy a py_ előtaggal van ellátva. A Python.h fejléc bemásolja (include-olja) a következő rendszer-fejlécfájlokat: stdio.h, strhing.h, errno.h, stdlib.h.

A következő lépés, hogy létrehozzunk egy C függvényt, ami akkor hívódik meg, ha a python interpreter span.system(string) formájú utasításokkal találkozik.

static PyObject* spam_system (PyObject* self, PyObject* args) { char *command; int sts; if (!PyArg_ParseTuple(args, "s", &amp;command)) return NULL; sts = system(command); return Py_BuildValue("i", sts); }

Megjegyzés: A C-beli és a Pythonbeli függvényneveknek semmi közük egymáshoz. Később majd meg kell adnunk egy "táblázatot", amiben egymáshoz rendeljük a Pythonbeli és a C-beli függvényneveket. Érdemes azonban bővítőfüggvényeinket modulnév_függvénynév alakban elnevezni, hogy a C-beli névnek legyen köze a pythonbeli névhez.

A bővítőfüggvényeknek mindíg két paramétere van: az elsőt konvenció szerint self-nek, a másodikat args-nak hívják.

Az első paraméter (a self) jelen esetben mindig NULL lesz, hiszen nem beépített objektum-metódust írtunk, hanem csak egy függvényt (ilyenkor nincs objektum amin dolgozunk).

A második paraméter (az args) egy python "tuple" adatstruktúrára mutató pointer (a tuple lényegében egy érték N-es, ahol N akármekkora lehet). Ennek a struktúrának minden eleme egy a függvénynek híváskor átadott paraméternek felel meg. Az argumentumok maguk is python objektumok, hiszem python kódból hívtuk meg az eljárást. Ezért ha bármit akarunk velük C-ben végezni, előbb át kell alakítani őket C adatstruktúrákká. Erre az átalakításra a Py_ParseTupple függvényt használhatjuk. Első paramétere egy tupple, második paramétere egy formátum string (hasonlóan pl. a scanf függvényhez) a többi paramétere megfelelő számú C változóra mutató pointer, amikbe a python objektumok kicsomagolását végzi a függvény.

Hiba esetén nullát ad értékül, siker esetén nullától különböző értéket.

Megjegyzés: Figyeljük meg, hogy a függvényünk statikus tárolási osztályú. Erre azért van szükség, hogy nehogy véletlenül függvényünk neve ütközzön más interpreterbeli vagy modulbeli függvénnyel.

Alapszabály, hogy egy bizonyos kivételtől eltekintve minden szimbólum statikus a modulok kódjában! Az egy nem statikus függvényre azért van szükség, hogy azt a modulon kívülről meghívhassa az interpreter a modul inicializálásakor, de erről majd később.

Kitérő: hibák, kivételek

Fontos konvenció az egész python interpreterben, hogy ha egy függvény hibát észlel, akkor hibakóddal tér vissza és beállít egy kivételt is.

A kivételek statikus globális változóban tárolódnak az interpreterben. Három ilyen változó van:

Ahhoz, hogy megtudjuk volt-e kivétel, nem kell ellenőriznünk mindig az első változó értékét, hiszen a fentebbi konvenció értelmében minden függvény visszatérési értékében is jelzi, ha volt hiba.

Szabály még, hogy csak akkor piszkáljuk a kivétel változókat, ha mi vagyunk az első rutin aki a hibát észreveszi, illetve ha a hibát ténylegesen tudjuk kezelni. Ha kezelni nem tudjuk, akkor egyszerűen mi is térjünk vissza NULL értékkel.

A kivételek kezeléséhez a Python API több függvényt is biztosít számunkra:

Egyetlen fenti függvény paramétereire sem kell a Py_INCREF makrot meghívni. Hogy ez miért fontos, arról később, a szemétgyűjtővel való együttműködés témakörében.

A PyErr_Occured függvény segítségével tesztelni lehet, hogy egy adott kivételt kiváltottak-e. A függvény meghívása nem szünteti meg a kivételes helyzetet.

Ha a hibát teljesen saját magunk akarjuk kezelni, akkor a kivételt törölni tudjuk a PyErr_Clear() függvénnyel. Egyéb esetben ne használjuk a függvényt.

Ha a memóriafoglalás sikertelen C kódban, azt mindenképpen jelezzük kivétellel, közvetlenül a sikertelen memóriafoglalás helyén. Ezt a PyErr_NoMemory() hívással könnyen megtehetjük. Ne felejtsünk el NULL-lal visszatérni.

Mindenképpen takarítsuk magunk után össze a szemetet (Py_DECREF() vagy Py_XDECREF() használatával) ha hibajelzést adunk vissza!

Hogy hiba esetén milyen kivételt váltunk ki, az teljesen a mi döntésünk. A beépített python kivételeknek mind megvan a C-beli előre deklarált megfelelője (pl. PyExc_ZeroDevisionError), amiket szabadon használhatunk. Persze a használt kivételt okosan kell megválasztani: -- ne használjuk PyExc_TypeError-t ha egy file megnyitásának a sikerteleségét akarjuk jelezni (ez valószínűleg PyExc_IOError lenne). Ha az argumentlista elemeinek típusával van valami baj, használjunk PyExc_TypeError-t. Ha valamely paraméter kívül esik a megengedett értékek halmazán, akkor valószínűleg a PyExc_ValueError kivétel a megfelelő.

Definiálhatunk saját modulspecifikus kivételt is, pl.

static PyObject* SpamError ;
és aztán ezt az objektumot a modul inicializáló függvényében inicializálhatjuk egy kivételpéldánnyal.

Egyenlőre kihagyva a hibaellenőzést:

void initspam() { PyObject *m, *d; m = Py_InitModule("spam", SpamMethods); d = PyModule_GetDict(m); SpamError = PyErr_NewException("spam.error", NULL, NULL); PyDict_SetItemString(d, "error", SpamError); }
Figyeljük meg, hogy a kivételobjektum neve spam.error. A PyErr_NewException() eljárás használható új Exception ősosztályú osztály definiálásához (a függvénynek megadható más ősosztály is, de ez az alapértelmezés).

Vissza a példához

Az előbbiek fényében már érthető a

if (!PyArg_ParseTuple(args, "s", &command)) return NULL;
elágázás: ha nem sikerült egy string típusú paramétert kicsomagolni, akkor visszatérünk NULL-lal, ezzel hibát jelezve a hívó fél felé. A PyArg_ParseTuple függvény beállította a megfelelő kivételt, ezért a hibakezeléssel más dolgunk nincsen. A command változó értékét csak olvasni szabad, úgyhogy const char*-nak kellene lennie!!!

A következő sor meghívja a system rendszerhívást a frissen kibányászott code paraméterrel, majd a Py_BuildValue() függvény segítségével a rendszerhívás értékét Python objektummá alakítja, aztán visszatér ezzel az objektummal.

Ha függvényünket úgy szeretnénk megírni, hogy ne adjon vissza értéket (mint C-ben a void függvények), akkor a következőket írhatnánk:

Py_INCREF(Py_None); return Py_None;
A Py_None az egy speciális Python objektum C-s neve. Ez az objektum általában hibát jelent, mint láttuk.

A modul metódustáblázata és inicializáló függvénye

Most megnézzük hogy a fent elkészült C kódot hogyan tudjuk a Python interpreter számára elérhetővé tenni (pl. még nem adtunk nevet a modulnak, és a függvényünknek sem). Először deklaráljunk egy táblázatot, amiben az általunk készített függvények neve és címe van:

static PyMethodDef SpamMethods[] = { ... {"system", spam_system, METH_VARARGS, "Végrehajt egy shell parancsot"}, ... {NULL, NULL, 0, NULL} /* Sentinel */ };
A táblázatot static-nak deklaráltuk a névütközések elkerülése végett.

Az első mező a függvényünk Python-beli neve, a második az impelementáló C eljárás címe, a harmadik mező pedig a hívási konvenciót jelzi. Értéke rendszerint METH_VARARGS vagy METH_VARARGS | METH_KEYWORDS.

Ha a hívási konvenciót METH_VARARGS-ra állítottuk, akkor a C függvény a paramétereket egyetlen "tupple" objektumba összefogva kapja meg, és az értékeket Py_ParseTupple hívással veheti ki belőle. Ha a METH_KEYWORDS jelzőt is beállítottuk, akkor a C függvénynek egy harmadik paramétert is kell fogadnia, ami egy "szótár" (más néven hash, dictionary), ami a kulcsszó alapján átadott paramétereket tartalmazza.

A fenti metódustáblát át kell adnunk az interpreternek a modul inicializáló függvényében. Az inicializáló függvény neve initmodulnév legyen, és ez a függvény legyen az egyetlen nem statikus entitás a modul kódjában. Ha C++-szal fordított Python interpretert használunk, akkor a függvényt extern C-nek is kell deklarálni.

void initspam() { (void) Py_InitModule("spam", SpamMethods); }

Amikor Python programunk először importálja a spam modult, akkor az initspam függvény meghívódik. Ez meghívja tovább a Py_InitModule függvényt. Ez utóbbi létrehozza a "modul objektumot", amit behelyez a sys.modules szótárba "spam" néven, illetve a második paramétere (a metódustábla alapján) behelyezi a C függvényeket a létrehozott modul objektumba. Ha a modult nem tudja inicializálni, akkor végzetes hibával leáll, ezért a hívónak nem kell hibát kezelnie.

Ha nem bővíteni akarjuk az interpretert, hanem beágyazni valami más aplikációba (pl. az aplikáció scriptnyelvének), akkor nem teljesen így kell eljárnunk, de ezzel most nem foglalkozunk.

A modul fordítása/szerkesztése (compilation és linkage)

Még két dolgot kell megtennünk, hogy modulunkat használni is tudjuk:

A szerkesztés kétféleképpen történhet: dinamikusan és statikusan. A dinamikus modulbetöltés használata kényelmesebb, de platform specifikus hogyan csinálhatunk dimamikusan betölthető könyvtárat, ezért ezt a módszert nem ismertetem. Akit érdekel, az utánanézhet itt

Ha a statikus linkelést választjuk, akkor hozzá kell nyúlnunk a Python interpreter setup fájljához, és újra kell fordítanunk az egész interpretert. Unix-ok alatt szerencsére ez nagyon könnyű: másoljuk modulunk forrását spammodule.c néven az interpreter forrás Modules/ könyvtárába, és adjuk hozzá a Modules/Setup.local fájlhoz a

spam spammodule.o
sort, és fordítsuk az interpretert a szokásos make paranccsal.

Megjegyzés: a dinamikusan betölthető modulok fordítását a Python 2.0 verzió óta a distutils csomag támogatja.

Ha a modulunknak szüksége lenne egyéb más külső programkönyvtárakra (pl. az Xlib-re), akkor a szükséges könyvtárakat is felsorolhatjuk a Modules/Setup.local fájlban:

spam spammodule.o -lX11

Python függvények hívása C kódból

Eddig arra koncentráltunk, hogy C függvényeinket elérhessük Python kódból. Néha azonban szükség lehet rá, hogy meghívjunk Python-ban írt kódot a bővítőmodulunkból. Erre akkor lehet például szükség, ha a modulunk "callback" funkcionalitást akar használni, például amikor egy ablakozós rendszerhez írunk Python bővítőmodult. Ilyenkor a felhasználó megad egy Python függvényt, és szeretné, hogy egy adott ablakozó rendszerben detektált esemény esetén ez a függvény lefusson.

Szerencsére ez is nagyon könnyű. Először is, a modulunknak szüksége van egy hívható Python objektumra. Ezt kivülről egy Python kódból hívott függvény adhatja át paraméterül. Ezután a modulunk elrakhat egy referenciát az átadott hívható objektumra egy globális változóban, és hívhatja, amikor csak szükséges. Ne felejtsük el Py_INCREF-elni a kapott referenciát, különben a szemétgyűjtő begyűjtheti a kapott objektumot, hiszen nem tudja hogy mi eltettünk belőle egy példányt.

A következő bővítőfüggvény példa lehet rá, hogyan adjuk át a modulnak hívható objektumra mutató referenciát:

static PyObject *my_callback = NULL; static PyObject *my_set_callback(PyObject *dummy, PyObject *args) { PyObject *result = NULL; PyObject *temp; if (PyArg_ParseTuple(args, "O:set_callback", &temp)) { if (!PyCallable_Check(temp)) { PyErr_SetString(PyExc_TypeError, "parameter must be callable"); return NULL; } Py_XINCREF(temp); /* Add a reference to new callback */ Py_XDECREF(my_callback); /* Dispose of previous callback */ my_callback = temp; /* Remember new callback */ /* Boilerplate to return "None" */ Py_INCREF(Py_None); result = Py_None; } return result; }
A függvény úgy van megírva, hogy a hívásikonvenciója METH_VARARGS legyen.

Később, ha ideje meghívni az előbbi eljárás által eltárolt Python objektumot, akkor ezt a Py_CallObject() hívással tehetjük meg. Ennek a függvénynek két PyObject típusú paramétere van: az első az objektum amit hívni akarunk, a második a hívás argumentumlistája. Ezért az első argumentum hívható objektumot, a másodiknak Python tuple objektumot kell hogy tartalmazzon. Ilyen tuple példányt könnyen előállíthatunk a Py_BuildValue() függvénnyel, ha a formátumstringet zárójelbe tesszük, pl.

int arg; PyObject *arglist; PyObject *result; ... arg = 123; ... /* Time to call the callback */ arglist = Py_BuildValue("(i)", arg); result = PyEval_CallObject(my_callback, arglist); Py_DECREF(arglist); if(result == NULL) return NULL ; /* itt használhatjuk a visszatérési értéket */ Py_DECREF(result)

Az argumentumlista referenciaszámlálóját csökkenteni kell, hiszen csak ideiglenes változónak hoztuk létre. A result változó értéke vagy a Python kód visszatérési értékét tartalmazza, vagy NULL-t ha hiba történt a kiértékéelés közben. Ha result értéke nem NULL, és nem is akarjuk eltárolni később, akkor mindenképpen csökkenteni kell a referenciaszámlálójának az értékét. NULL visszatérési érték esetén tanácsos a bővítőfüggvény kódjából is hibajelzéssel visszatérni, hogy az interpreter észrevegye a hibát és jelezhesse a felhasználó felé.

Bővítő függvények paramétereinek átalakítása C adat struktúrákká

Erre a feladatra a

int PyArg_ParseTuple(PyObject *arg, char *format, ...);
függvény való. Ez a formatformátumstring alapján kiszedi az arg paraméterben található Python tuple objektum által tartalmazott további Python objektumok értékeit, és eltárolja őket a további átadott paraméterekbe.

A formátum string leírását nézzétek meg a dokumentácóban.

Példák:

int ok; int i, j; long k, l; char *s; int size; ok = PyArg_ParseTuple(args, ""); /* No arguments */ /* Python call: f() */ ok = PyArg_ParseTuple(args, "s", &s); /* A string */ /* Possible Python call: f('whoops!') */ ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */ /* Possible Python call: f(1, 2, 'three') */ ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size); /* A pair of ints and a string, whose size is also returned */ /* Possible Python call: f((1, 2), 'three') */ { char *file; char *mode = "r"; int bufsize = 0; ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize); /* A string, and optionally another string and an integer */ /* Possible Python calls: f('spam') f('spam', 'w') f('spam', 'wb', 100000) */ } { int left, top, right, bottom,&; ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)", &left, &top, &right, &bottom, &h, &v); /* A rectangle and a point */ /* Possible Python call: f(((0, 0), (400, 300)), (10, 10)) */ } { Py_complex c; ok = PyArg_ParseTuple(args, "D:myfunction", &c); /* a complex, also providing a function name for errors */ /* Possible Python call: myfunction(1+2j) */ }

Bővítő függvények kulcsszavas paramétereinek átalakítása C adat struktúrákká

TODO :-)

C adatstruktúrák Python objektummá alakítása

Erre a feladatra a

PyObject *Py_BuildValue(char *format, ...) ;
függvény használható. Úgy goldoljuk a függvényre, mint a PyArg_ParseTuple() párjára, ami az ellenkező irányú konverziót csinálja.

Példák:

Py_BuildValue("") None Py_BuildValue("i", 123) 123 Py_BuildValue("iii", 123, 456, 789) (123, 456, 789) Py_BuildValue("s", "hello") 'hello' Py_BuildValue("ss", "hello", "world") ('hello', 'world') Py_BuildValue("s#", "hello", 4) 'hell' Py_BuildValue("()") () Py_BuildValue("(i)", 123) (123,) Py_BuildValue("(ii)", 123, 456) (123, 456) Py_BuildValue("(i,i)", 123, 456) (123, 456) Py_BuildValue("[i,i]", 123, 456) [123, 456] Py_BuildValue("{s:i,s:i}", "abc", 123, "def", 456) {'abc': 123, 'def': 456} Py_BuildValue("((ii)(ii)) (ii)", 1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))

Referenciaszámlálás

A C/C++ nyelvekben a programozó felelőssége hogy a lefoglalt memóriát felszabadítsa, ha már nincs a programnak szüksége rá. Ha ezt elfelejtené, akkor a program "szivárogni" kezd, egyre nő a memória felhasználása. Ez hiba, hiszen fölöslegesen sok rendszererőforrást használ így a program. További hibalehetőség a már egyszer felszabadított memóriaterületek használata: ilyenkor legjobb esetben is elszáll a program, rosszabb esetben megbízhatatlanul működik.

A memóriaszivárgások leggyakoribb okozói a kódban ritkán előfoduló végrehajtási utak mentén találhatóak. Például vegyünk egy függvényt, ami működésének elején memóriát foglal, használja azt, végül felszabadítja. Ezután a változó programspecifikáció miatt bekerül egy teszt a számolós részbe, ami számolási hiba esetén hibajelzéssel visszatér. Könnyű ilyenkor megfeledkezni a memória felszabadításáról. Az ilyen hibák sokáig lapulhatnak észrevétlen, mivel a futás során ritkán fordulnak elő, és egyetlen nyomuk, hogy egy kicsit nőtt a program memóriafoglalása.

A Python nyelvet amikor tervezték, szempont volt hogy a memóriakezelés nyűgjét levegyék a programozó válláról. Ezt úgy érték el hogy az interpreter minden objektumra nyilvántartja, hogy az adott időpillanatban hány referencia mutat rá. Ezt úgy éri el, hogy minden értékadáskor automatikusan növeli, illetve csökkenti a megfelelő objektumok referenciaszámlálóját. Ha egy objektum referenciaszámlálója nulla, akkor már biztosan felszabadítható az általa foglalt hely, mert nincs senkinek referenciája az objektumra.

Mivel bővítőmodulok írásakor mi is gyakran dolgozunk Python objektumokkal. Gyakran eltároljuk őket, példányokat hozunk létre, törlünk le. Mivel a mi bővítőmodulunkban történt eseményekről az interpreter közvetlenül nem szerez tudomást, ezért a referenciaszámlálók állítgatását sem tudja megoldani helyettünk. A rendszer helyes működése érdekében ezért nekünk kézzel kell karbantartani a referenciaszámlálókat.

A tipikus referencia számláló rendszerek becsaphatók körben egymásra referenciát fenntartó objektumokkal. A Python interpreter opcionálisan tartalmaz egy kördetektort, de ez kikapcsolható.

Referenciaszámlálás Python-ban

A feladat elvégzésére két makrot biztosít nekünk a környezet: Py_INCREF(x) és Py_DECREF(x). Ezek kezelik a paraméterül kapott objektumok referenciaszálálóját, és gondoskodnak róla hogy amint a referenciaszáláló értéke eléri a nullát az objektumok felszabaduljanak.

Megjegyzés: Igazából a memória felszabadítása nem direktben történik, hanem az objektumohoz tartozó felszabadító függvényt hívja meg a rendszer. Ez az indirekció lehetővé teszi, hogy tényleges (és költséges) felszabadítás helyett az objektum egy gyűjtőtárolóba (pool) jusson, és ott várja az esetleges újrafelhasználást.

Marad a kérdés: mikor hívjuk a fenti makrókat? Ehhez néhány fogalmat tisztázni kell.

Maguknak az objektumoknak nincs "tulajdonosa", csak az objektumokra mutató referenciáknak. A referencia tulajdonosának kötelessége meghívni a referenciára a Py_DECREF makrot amikor már nincs szüksége többé a referenciára. A referencia tulajdonjoga viszont továbbadható. Három féle dolgot csinálhatunk a birtoklott referenciával:

Lehetőség van referencia kölcsönzésre. Kölcsönadni a tulajdonos, vagy kölcsönző tud. A kölcsönző nem hívhat Py_DECREF-et, hiszen ezzel potenciálisan megszüntethetné magát az objektumot amire a referenciája mutat. Sőt, valahogy biztosítania kell, hogy csak addig használja a kölcsönkapott refereciát, amíg a kölcsönadó birtokolja azt, hiszen addig a mutatott objektum létezése garantált (legalább egy a referenciaszámlálója). Ha a kölcsönző nem tárolja el a kölcsönzött referenciát, akkor többnyire biztonságban van, hiszen a saját kódja lefutásának idejére senki más nem fér az objektumhoz, így nem is törölheti azt.

Megjegyzés: A fenti utolsó kitétel nem teljesen igaz: látni fogunk még arra példát, hogy kölcsönzött referencia által mutatott objektum megszűnik még a függvény futása alatt anélkül hogy mi Py_DECREF()-et hívnánk rá.

A kölcsönzés előnye, hogy nem kell minden lehetséges kódlefutási úton figyelni a referenciaszámlálóra (hiszen ha növeltük, akkor tulajdonossá váltunk, és csökkentenünk is, mégpedig minden lehetséges kódlefutái úton). Hátránya viszont, hogy néhány ritka esetben egy látszólag hibátlan kód olyan objektumokat használhat, amiket már felszabadítottak.

A kölcsönzött referenciát átalakíthatjuk tulajdonlott referenciává egy Py_INCREF() hívással. Ilyenkor persze ránk ruházódnak a tulajdonosi kötelezettségek is: figyelnünk kell arra, hogy el is engedjük a referenciát majd valamikor.

Referenciatulajdonlási szabályok

Amikor egy C függvény referenciát ad át vagy vesz át, akkor a függvény specifikációjának a része, hogy a tulajdonosi kötelezetségeket is átadja vagy átveszi-e a referenciára vonatkozóan.

A legtöbb függvény, ami referenciát ad vissza, a tulajdonlást is átruházza a hívóra. Különösen igaz ez azokra a függvényekre, amiknek egy objektum létrehozása a feladata. Ilyen függvények például a PyInt_FromLong() és a Py_BuildValue(). Sőt előfordulhat, hogy egy objektumkészítő függvény nem hoz létre új értéket -hanem mondjuk egy régit ad vissza a saját gyorstárából- de a tulajdonlást akkor is átadja. Erre az utóbbira példa a PyInt_FromLong(), ami nyilvántartja a leggyakrabban használt egész értékeket, és csak szükség esetén hoz létre belőlük új példányt (az egész számok konstans objektumok, ezért biztonságosan megoszthatóak több eljárás között).

A legtöbb olyan függvény is átadja a tulajdonjogot, ami egy objektum részeit adja vissza, pl. a PyObject_GetAttrString(). Itt azonban nem olyan egyértelmű a helyzet, mert vannak gyakran használt kivételek: a PyTupple_GetItem(), a PyList_GetItem(), a PyDict_GetItem(), és a PyDict_GetItemString csak kölcsönadja a referenciát a tuplétól, listától és a szótártól.

A PyImport_AddModule függvény is csak kölcsönzött referenciát ad vissza, bár gyakran új objektumot hoz létre. Ez azért lehetséges, mert az általa létrehozott objektumok bekerülnek a sys.modules szótárba, és ettől kölcsönöz a hívó.

Amikor egy függvénynek paramétként adunk át objektumot, akkor legtöbbször kölcsönözzük azt, és ha a hívó tárloni akarja, átveszi a referencia tulajdonjogát egy Py_INCREF hívással. Ez alól a szabály alól pontosan két kivétel van: PyTuple_SetItem() és PyList_SetItem(). Ezek a függvények átveszik a referencia tulajdonjogát, még akkor is ha hiba miatt nem tudnak lefutni.

Amikor Python kódból hívunk C függvényt, akkor a C függvény csak kölcsönzi a referenciát amit kapott. A hívó kód tulajdonosa az átadott referenciának ezérta paraméterül kapott objektum élettartama garantált a C függvény visszatéréséig. Ezért Pythontól paraméterül kapott referenciát csak akkor kell Py_INCREF()-elni, ha el akarjuk tárolni későbbi használatra, vagy tovább szeretnénk adni.

A C függvény által a Python interpreternek visszaadott referencia értéke kötelező hogy tulajdonlott referencia legyen, és a tulajdonjog átszáll a hívó python kódra (tehát nekünk nem kell sőt tilos is Py_DECREF-enli).

Vékony jég - a referenciaszámlálás buktatói

Van néhány olyan helyzet, amikor a kölcsönzött referenciák látszólag ártalmatlan használata problémákhoz vezethet. Ezek a veszélyes helyzetek mind arra vezethetők vissza, hogy a Python interpreter implicit meghívódik, igy előfordulhat hogy a kölcsönzött referencia tulajdonosa megszünteti a tulajdonlását, így maga a referált objektum is megszűnhet.

Az első és legfontosabb dolog amiről tudnunk kell az az, hogy a Py_DECREF() hívás egy második tetszőleges objektumon okozhatja egy látszólag független kölcsönzött referencia által mutatott objektum megszűnését. Nézzük például a következő függvényt:

void bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); /* BUG! */ }
Először kölcsönzünk egy referenciát a kapott lista első elemére, a másodikat nullára állítjuk, aztán kiírjuk az kölcsönzött referencia értékét. Ártalmatlannak látszik, de nem az.

Nézzük meg, mi is történik a PyList_SetItem belsejében. A lista tulajdonosa az elemeire mutató referenciáknak, így amikor a második elemet felülírjuk -ha nem volt a második elemre más referencia-, megszűnik a második elem által referált objektum. Tegyük fel hogy ez az objektum egy felhasználó által definiált objektum volt, és volt saját __del__ metódusa. Tegyük fel hogy az utolsó referencia szünt meg list[1]-re, így a __del__ metódust lefuttatja a környezet.

Mivel a __del__ metódust Pythonban írták, ezért tetszőleges kód lefuthat, például olyan is ami értvényteleníti a listaobjektum első elemét. Például ha van referenciája a listaobjektumra, tartalmazhat valahol egy del list[0] sort, így az itemváltozóban lévő referencia érvénytelen objektumra mutathat.

Amit rájöttünk a hibára a javítás könnyű: átmenetileg Py_INCREF()-elni kell az item által mutatott objektumot. A heyes függvény tehát így néz ki:

void no_bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); Py_INCREF(item); PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); Py_DECREF(item); }
Ez a példa igaz történet alapján született: a régebbi Python interpreterekben fellelhetők voltak ilyen hibák.

A hiba oka az volt tehát, hogy egy "set" típusú művelet okozhatja objektum megszűnését, ami okozhatja más objektumok megszűnését.

A második kölcsönzött referenciák által okozott hibaeset egy olyan változat, ahol a hibát a szálak használata okozza. Normál esetben a különböző szálak nem interferálhatnak egymással a Python interpreterben, mert egy globális zár (lock) védi a Python objektumterét. Időlegesen lehetséges ennek a zárnak a felengedése a Py_BEGIN_ALLOW_THREADS makro segítségével. A felengedés addig tart, amíg a Py_END_ALLOW_THREADS makrot meg nem hívjuk. Az átmeneti felengedés gyakran előfordul blokkoló I/O hívások körül, hogy amíg az egyik szál I/O-ra vár, a többi dolgozhasson. A következő függvénynek nyilvánvalóan ugyanaz a problémája mint az első példában szereplőnek:

void bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); Py_BEGIN_ALLOW_THREADS ...some blocking I/O call... Py_END_ALLOW_THREADS PyObject_Print(item, stdout, 0); /* BUG! */ }

Itt is az item változó feltöltése és használata között akármilyen kód lefuthat, például olyan is, ami okozhatja item utolsó tulajdonlott referenciájának megszűnését.

C API készítése a bővítőmodulhoz

A bővítőmodulunk immár elérhető Python kódból. De mi történik, ha egy másik bővítőmodulból is használni szeretnék a szolgáltatásait? Ilyen eset könnyel előfordulhat ha egy új típust definiáltunk például.

Első ránézésre könnyűnek tűnik a probléma: egyszerűen ne statikusnak deklaráljuk a bővítőfüggvényeket, csináljuk hozzájuk egy fejléc fájlt, és máris lehet más modulból használni a függvényeinket. A helyzet azonban nem ilyen egyszerű. Egyrészt gondot okozhatnak a függvénynévütközések, másrészt ez a metódus nem működik a dinamikus modulbetöltéssel.

Dinamikus betöltés esetén a modul szimbólumainak láthatósága platformfüggő. Windows alatt például minden szimbólum egy globálisan elérhető névtérbe kerül, de a legtöbb Unixszerű rendszer esetén explicit importálni kell. Mégha ezt a problémát meg is oldanánk valahogy, baj lehet a modulok betöltési sorrendjével, hiszen lehet, hogy a modulunkat használó másik modult hamarabb tölti be a rendszer mint a miénket. A Python ezért egy másik rendszert dolgozott ki a C API függvények exportálására.

A Python bevezet egy speciális adattípust: a CObject típust. Ennek célja, hogy biztosítsa hogy modulok biztonságosan cserélhessenek egymással információt pointerek formájában. A CObject objektumokba egy void* pointert tudunk belecsomagolni. CObject típusú objektumot csak a Python C API segítségével hozhatunk létre és érhetünk el (tehát Python kódból csak bővítőmodul útján), de átadhatjuk paraméterként ugyan úgy mint a PyObject-eket. Konkrétan nevet adhatunk nekik a bővítőmodulunk névterében, más bővítőmodulok pedig elérhetik őket a szokásos módon miután importálták a bővítőmodulunkat.

Még így is többféle módszer lehetséges C struktúrák exportjára. Lehetséges, hogy minden exportálandó struktúrához egy külön CObject-et rendelünk saját névvel, de az is lehet, hogy minden exportálandó struktúrát egy tömbbe (vagy más adatstruktúrába) csomagolunk, és csak ez a tömb (vagy más struktúra) kap egy saját CObject-et persze névvel együtt. Ebben az esetben a modul kód írójának kell mindenféle ezközt nyújtania hogy kliensei elérhessék a csomagolt C pointereket.

A következő példa az utóbbi megközelítést mutatja be, hiszen a programkönyvtárak általában ezt a megközelítést követik hogy minél kényelmesebbé tegyék a használatukat. Az összes exportálandó C struktúrát egy void*-okból álló tömbbe teszi (jelen példában csak egy darabot!), és aztán ehhez a tömbhöz rendel egy CObject értéket. A modulhozt artozó fejlécfájl tartalmaz makrokat a tömb elemeinek kinyeréséhet, illetve a modul importálásához, ezért a modul használata a kliens kód számára nagyon könnyű.

Az exportáló modul a spammodulon alapul. Itt a spam.system hívás majd nem közvetlenül a C programkönyvtár system függvényét hívja meg, hanem egy PySpam_System() nevű függvényt. Ez utóbbi végzi a tényleges munkát, és igazából akármilyen komplex lehetne, most csak a példa rövidségének a kedvéért ilyen egyszerű. Az eredeti spam_system függvény csak a paraméterek "fordítását" végzi C struktúrákká.

A PySpam_System() függvény egy egyszerű C függvény, ami static-nak van deklarálva mint minden más (persze az initspam-től eltekintve).

static int PySpam_System(char *command) { return system(command); }

A spam_system függvényt triviálisan módosítottuk csak:

static PyObject * spam_system(PyObject *self, PyObject *args) { char *command; int sts; if (!PyArg_ParseTuple(args, "s", &command)) return NULL; sts = PySpam_System(command); return Py_BuildValue("i", sts); }

A modul elejére közvetlenül a

#include "Python.h"
sor után beszúrjuk a következő két sort:
#define SPAM_MODULE #include "spammodule.h"
A #define-t arra használuk, hogy tudassuk a spammodule.h fejlécfájllal hogy most az exportáló modulba kerül beillesztésre, nem a klienskódba. Végül a modul inicializáló függvényének gondoskodnia kell az exportálandó objektumokat tartalmazó tömb CObject-be csomagolásáról, és tényleges exportálásáról.
PyMODINIT_FUNC initspam(void) { PyObject *m; static void *PySpam_API[PySpam_API_pointers]; PyObject *c_api_object; m = Py_InitModule("spam", SpamMet&; /* Initialize the C API pointer array */ PySpam_API[PySpam_System_NUM] = (void *)PySpam_System; /* Create a CObject containing the API pointer array's address */ c_api_object = PyCObject_FromVoidPtr((void *)PySpam_API, NULL); if (c_api_object != NULL) PyModule_AddObject(m, "_C_API", c_api_object); }
Vegyük észre, hogy a PySpam_API tömb static-nak van deklarálva, különben értékét vesztené az inicializáló függvény lefutása után.

A munka nagyrésze a spammodule.h fájlba kerül, ami így néz ki:

#ifndef Py_SPAMMODULE_H #define Py_SPAMMODULE_H #ifdef __cplusplus extern "C" { #endif /* Fejlécfájl a spammodule-hoz */ /* C API eljárások */ #define PySpam_System_NUM 0 #define PySpam_System_RETURN int #define PySpam_System_PROTO (char *command) /* A C API-ban található pointerek teljes száma */ #define PySpam_API_pointers 1 #ifdef SPAM_MODULE /* Ezt a részt használjuk a spammodule.c fordításakor (emlékszel a #define-ra? */ static PySpam_System_RETURN PySpam_System PySpam_System_PROTO; #else /* Ezt a részt használják a spammodul C API-t használni kívánó kliensek */ static void **PySpam_API; #define PySpam_System \ (*(PySpam_System_RETURN (*)PySpam_System_PROTO) \ PySpam_API[PySpam_System_NUM]) /* -1-et ad vissza, és kivételt állít be hiba esetén, egyébként 0-t ad vissza. */ static intimport_spam(void){ PyObject *module = PyImport_ImportModule("spam"); if (module == NULL) return -1 ; PyObject *c_api_object = PyObject_GetAttrString(module, "_C_API"); if (c_api_object == NULL) return -1; if (PyCObject_Check(c_api_object)) PySpam_API = (void **)PyCObject_AsVoidPtr(c_api_object); Py_DECREF(c_api_object); return 0; } #endif #ifdef __cplusplus } #endif #endif /* !defined(Py_SPAMMODULE_H) */

Ezek után a kliens dolga igen egyszerű:

... #include "spammodule.h" ... PyMODINIT_FUNC initclient(void) { PyObject *m; Py_InitModule("client", ClientMethods); if (import_spam() &lt 0) return; /* additional initialization can happen here */ }

Ennek a megközelítésnek legnagyobb hátránya, hogy a modulunkhoz tartozó fejlécfájl nagyon bonyolult, de mivel a fejléc struktúrája lényegében független az exportálandó szimbólunok (void* pointerek) számától, ezért elég egyszer megtanulni.

Érdemes megjegyezni hogy a CObject objektumok mindezen funkcionalitás mellet setítséget nyújtanak az általuk tartalmazott void* mutatók deallokálásához. Erről bővebbet a Python C API leírásában

Új típusok definiálása

A Python támogatja a kiegészítő modul íróját új típus definiálásában, amiket Python kódból lehet kezelni, éppúgy mint a sztringeket vagy a listákat.

Ez nem nehéz, minden kiterjesztett típus egy mintát követ, de néhány részlet megértésére szükség van, mielőtt belevágnánk.

Megj: az új típusok definiálása nagy változáson ment át a 2.2-es Pythonban. Itt a 2.2-es verzió működését írjuk le, ez eltér a korábbiaktól.

Az alapok

A Python futásidőben minden Python objektumot mint PyObject* típusú változót lát. Ez a PyObject nem egy nagyszabású objektum, csak egy referenciaszámlálót és egy mutatót tartalmaz az objektum típusobjektumára. Ez a típusobjektum gondoskodik arról, hogy melyik (C) függvény mikor hívódik meg, például ahogy egy attribútum lekérdezése vagy egy objektum megszorzása egy másikkal. Ezek a C függvények a típusmetódusok (type method), és megkülönböztetendők a [].append jellegű függvényektől, amik objektummetódusok (objekt method).

Tehát, ha új osztályt kell definiálni, akkor létre kell hozni egy típusobjektumot.

Ezt egy példán keresztül nézzük végig, itt egy minimális de teljes modul, ami új típust definiál.

#include <python .h> typedef struct { PyObject_HEAD /* Type-specific fields go here. */ } noddy_NoddyObject; static PyTypeObject noddy_NoddyType = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "noddy.Noddy", /*tp_name*/ sizeof(noddy_NoddyObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ 0, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*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 */ 0, /*tp_call*/ 0, /*tp_str*/ 0, /*tp_getattro*/ 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT, /*tp_flags*/ "Noddy objects", /* tp_doc */ }; static PyMethodDef noddy_methods[] = { {NULL} /* Sentinel */ }; #ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ #define PyMODINIT_FUNC void #endif PyMODINIT_FUNC initnoddy(void) { PyObject* m; noddy_NoddyType.tp_new = PyType_GenericNew; if (PyType_Ready(&noddy_NoddyType) < 0) return; m = Py_InitModule3("noddy", noddy_methods, "Example module that creates an extension type."); Py_INCREF(&noddy_NoddyType); PyModule_AddObject(m, "Noddy", (PyObject *)&noddy_NoddyType); }

Ez egyszerre kicsit nagy falat, de a kiterjesztésről szóló fejezetek ismeretében ismerősen mutat.

Nézzük az első részt, ez új:

typedef struct { PyObject_HEAD } noddy_NoddyObject;
Ezt fogja a Noddy objektum tartalmazni, ebben az esetben semmi többet mint amit minden Python objektum tartalmat, vagyis a referenciaszámlálót és a típusobjektum mutatóját, ezeket a mezőket a PyObject_HEAD makró adja. A makró oka az, hogy így jól lehet szabványosítani a szerkezetet, és könnyű hibakeresési lehetőséget ad speciális debug-mezők beépíthetőségével. Vegyük észre, hogy nincs pontosvessző a makró után, a pontosvesszőt ugyanis tartalmazza a makró. Erre oda kell figyelni, mert bizonyos rendszerek, fordítóprogramok panaszkodnak erre, és hibát jeleznek a dupla pontosvessző miatt, és ez ronthatja a kód hordozhatóságát.

Hasonlításképpen nézzük a Python szabványos egész típusának megfelelő definícióját:

typedef struct { PyObject_HEAD long ob_ival; } PyIntObject;
Menjünk tovább, a következő szakasz a példában a következő:
static PyTypeObject noddy_NoddyType = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "noddy.Noddy", /*tp_name*/ sizeof(noddy_NoddyObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ 0, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*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 */ 0, /*tp_call*/ 0, /*tp_str*/ 0, /*tp_getattro*/ 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT, /*tp_flags*/ "Noddy objects", /* tp_doc */ };
Ha most megnézzük a PyTypeObject definícióját az object.h fájlban, akkor azt látjuk, hogy sokkal több mezője van, mint ennek a definíciónak. A maradék mezők nullával lesznek kitöltve, a C fordítónak köszönhetően, szokás tehát csak az utolsó nem nulláig kiírni.

A következő sornál:

PyObject_HEAD_INIT(NULL)
fontos, hogy elméletileg annak kellene itt állnia, hogy
PyObject_HEAD_INIT(&PyType_Type)
csakhogy ezt sok C fordító nem fogadja el. A típusobjektum típusának kellene itt szerepelnie, amit egy nullmutatóval kerülünk meg, és utólag töltjük ki a mezőt a PyType_Ready() függvénnyel.

Az ob_size már nem használt mező, csak a bináris kompatibilitás miatt tartották meg. Mindig legyen nulla.

A tp_name mező a típusunk neve, ez jelenik meg a szöveges reprezentációban és néhány hibaüzenetben, például:

>>> "" + noddy.new_noddy() Traceback (most recent call last): File "", line 1, in ? TypeError: cannot add type "noddy.Noddy" to string

Vegyük észre, hogy a név egy ponttal elválasztva mind a modul nevét, mind a típus nevét tartalmazza. Itt a modul a noddy és a típus a Noddy.

sizeof(noddy_NoddyObject), /* tp_basicsize */
Így a Python tudja, hogy mennyi memóriát kell lefoglaljon PyObject_New() hívásnál.

A tp_itemsize mezőnek a sztringek és listák hosszához van köze, ebben a példában most hanyagoljuk, ahogy több metódust is utána.

Py_TPFLAGS_DEFAULT, /*tp_flags*/
Minden típusnak tartalmaznia kell egy flag konstanst. Ez a default konstans az aktuális verzióban minden tagot enged definiálni.

Egy dokumentációs sztringet is megadhatunk a tp_doc mezőben.

Most nézzük a típusmetódusokat, ami az objektumainkat meg fogja különböztetni a többitől. Most még nem implementálunk egyet sem ebben a példában, később fogjuk kiegészíteni. Addig is, azt szeretnénk, hogy létre tudjunk hozni egy új Noddy objektumot. Hogy az objektum létrehozását lehetővé tegyük, a tp_new függvényt implementálnunk kell. Ebben az esetben használhatjuk az alapértelmezett függvényt, amit a PyType_GenericNew() API hívás ad. A legegyszerűbb az volna, ha egyszerűen hozzárendelnénk ezt az API-t a tp_new híváshoz, de ezt hordozhatósági szempontok miatt érdemes elkerülni. Néhány fordítóprogram nem tud statikusan inicializálni tagot olyan függvénnyel, amit más C modulban definiáltak, úgyhogy ehelyett a tp_new függvényt úgy implementáljuk, hogy a GenericNew meghívása után még PyType_Ready()-t is hívunk.

noddy_NoddyType.tp_new = PyType_GenericNew; if (PyType_Ready(&noddy_NoddyType) < 0) return;
Minden más típusmetódus most NULL, a következő fejezetben fogunk róluk gondoskodni.

Minden más a kódban ismerős, kivéve azt a kis szakaszt az initnoddy() függvényben:

if (PyType_Ready(&noddy_NoddyType) < 0) return;
Itt inicializáljuk a Noddy típust, az ob_type már lenullázott értékével együtt. Most már bejegyezhetjük új típusunkat a modulkönyvtárba:
PyModule_AddObject(m, "Noddy", (PyObject *)&noddy_NoddyType);
Ez megengedi, hogy Noddy típusunkat példányosítsuk:
import noddy mynoddy = noddy.Noddy()
Nos igen, megvan az első saját típusunk. Igaz még nem tud semmit, nincsenek függvényei és adattagjai, de majd a következő fejezetekben megtöltjük.

Tehát egyelőre a fenti kódot bele kell tenni egy fájlba, legyen noddy.c, a következő sorokat egy noddy.py fájlba:

from distutils.core import setup, Extension setup(name="noddy", version="1.0", ext_modules=[Extension("noddy", ["noddy.c"])])
Amit aztán már használhatunk:
$ python setup.py build
A fordító egy noddy.so fájlt létre fog hozni, amit a megfelelő helyről elérhetővé téve már importálhatjuk és használhatjuk a Noddy objektumokat.

Adattagok és metódusok hozzáadása

Nézzük a következő kódrészletet:

#include #include "structmember.h" typedef struct { PyObject_HEAD PyObject *first; PyObject *last; int number; } Noddy; static void Noddy_dealloc(Noddy* self) { Py_XDECREF(self->first); Py_XDECREF(self->last); self->ob_type->tp_free((PyObject*)self); } static PyObject * Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { Noddy *self; self = (Noddy *)type->tp_alloc(type, 0); if (self != NULL) { self->first = PyString_FromString(""); if (self->first == NULL) { Py_DECREF(self); return NULL; } self->last = PyString_FromString(""); if (self->last == NULL) { Py_DECREF(self); return NULL; } self->number = 0; } return (PyObject *)self; } static int Noddy_init(Noddy *self, PyObject *args, PyObject *kwds) { PyObject *first=NULL, *last=NULL; static char *kwlist[] = {"first", "last", "number", NULL}; if (! PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist, &first, &last, &self->number)) return -1; if (first) { Py_XDECREF(self->first); Py_INCREF(first); self->first = first; } if (last) { Py_XDECREF(self->last); Py_INCREF(last); self->last = last; } return 0; } static PyMemberDef Noddy_members[] = { {"first", T_OBJECT_EX, offsetof(Noddy, first), 0, "first name"}, {"last", T_OBJECT_EX, offsetof(Noddy, last), 0, "last name"}, {"number", T_INT, offsetof(Noddy, number), 0, "noddy number"}, {NULL} /* Sentinel */ }; static PyObject * Noddy_name(Noddy* self) { static PyObject *format = NULL; PyObject *args, *result; if (format == NULL) { format = PyString_FromString("%s %s"); if (format == NULL) return NULL; } if (self->first == NULL) { PyErr_SetString(PyExc_AttributeError, "first"); return NULL; } if (self->last == NULL) { PyErr_SetString(PyExc_AttributeError, "last"); return NULL; } args = Py_BuildValue("OO", self->first, self->last); if (args == NULL) return NULL; result = PyString_Format(format, args); Py_DECREF(args); return result; } static PyMethodDef Noddy_methods[] = { {"name", (PyCFunction)Noddy_name, METH_NOARGS, "Return the name, combining the first and last name" }, {NULL} /* Sentinel */ }; static PyTypeObject NoddyType = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "noddy.Noddy", /*tp_name*/ sizeof(Noddy), /*tp_basicsize*/ 0, /*tp_itemsize*/ (destructor)Noddy_dealloc, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*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 */ 0, /*tp_call*/ 0, /*tp_str*/ 0, /*tp_getattro*/ 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ "Noddy objects", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ Noddy_methods, /* tp_methods */ Noddy_members, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ (initproc)Noddy_init, /* tp_init */ 0, /* tp_alloc */ Noddy_new, /* tp_new */ }; static PyMethodDef module_methods[] = { {NULL} /* Sentinel */ }; #ifndef PyMODINIT_FUNC/* declarations for DLL import/export */ #define PyMODINIT_FUNC void #endif PyMODINIT_FUNC initnoddy2(void) { PyObject* m; if (PyType_Ready(&NoddyType) < 0) return; m = Py_InitModule3("noddy2", module_methods, "Example module that creates an extension type."); if (m == NULL) return; Py_INCREF(&NoddyType); PyModule_AddObject(m, "Noddy", (PyObject *)&NoddyType); }

Kicsit hosszú, de látható, hogy az első Noddy kód kiegészítéséről van szó. Hozzáépítettünk adattagokat, függvényeket, és bázisosztályként való használhatóságot is tettünk bele.

Nézzük most apránként a változásokat. Egy új include sorunk van:

#include "structmember.h"
Attribútumkezelő függvényeket kapunk ebből a header fájlból.

Most több adattag van, a first, last és a number. A first és last változós Python sztringek, és neveket tartalmaznak, a number egy egész.

Most, hogy már adatot is kell kezelni, körültekintőbben kell megfogalmazni az allokációt és a deallokációt. Minimum a deallokációt implementálni kell:

static void Noddy_dealloc(Noddy* self) { Py_XDECREF(self->first); Py_XDECREF(self->last); self->ob_type->tp_free((PyObject*)self); }
Ezt az függvényt pedig ráakasztjuk a típusmetódusra:
(destructor)Noddy_dealloc, /*tp_dealloc*/
Ez a metódus csökkenti a referenciaszámlálót a két attribútumnál. Ezt a Py_XDECREF() függvényt azért használjuk, mert a first és last tagok lehetnek nullmutatók. Ha a csökkentés megvolt, akkor a tp_free függvényt, hogy felszabadítsa az objektum memóriáját. Vegyük észre, hogy ez az objektumtípus nem feltétlenül NoddyType, akár leszármazottja is lehet.

Biztosak akarunk lenni abban, hogy a first és last sztringek üres sztringként inicializálódnak, ezért a new metódust eszerint írjuk meg:

static PyObject * Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { Noddy *self; self = (Noddy *)type->tp_alloc(type, 0); if (self != NULL) { self->first = PyString_FromString(""); if (self->first == NULL) { Py_DECREF(self); return NULL; } self->last = PyString_FromString(""); if (self->last == NULL) { Py_DECREF(self); return NULL; } self->number = 0; } return (PyObject *)self; }
Aztán bejegyezzük ezt a függvényt is a tp_new mezőbe:
Noddy_new, /* tp_new */
Amennyiben nem foglalkozunk az adattagok inicializálásával, a Python alapértelmezett new művelete NULL kezdőértéket fog nekik adni.

A new függvény akkor hívódik meg, amikor példányosítunk, fogadhat paramétereket, akár pozícióval, akár változónévvel azonosítva, és mindig az új objektum a visszaadott érték. A new futása előtt a tp_alloc függvényt hívja meg, amit nem nekünk kell megírni, csak a PyType_Ready() függvényt meghívni, az fogja beállítani a lefoglalandó memóriát. Persze lehet kézzel is megadni az allokáló függvényt, de nem szokás.

Ilyen az inicializáló függvény a példában:

static int Noddy_init(Noddy *self, PyObject *args, PyObject *kwds) { PyObject *first=NULL, *last=NULL; static char *kwlist[] = {"first", "last", "number", NULL}; if (! PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist, &first, &last, &self->number)) return -1; if (first) { Py_XDECREF(self->first); Py_INCREF(first); self->first = first; } if (last) { Py_XDECREF(self->last); Py_INCREF(last); self->last = last; } return 0; }
Amit a tp_init mezőre akasztunk.
(initproc)Noddy_init, /* tp_init */
Az init függvény annyiban tér el a new-tól, hogy nincs garantálva a lefutása, vagyis felüldefiniálható. Szintén paraméterezhető, például kezdetiértékekkel. A hagyományos konstruktor fogalom a new és az init függvények összessége.

Sokféleképpen lehet elérhetővé tenni ezeket az attribútumokat, a legegyszerűbb mód, hogy tagdefiníciókat írunk:

static PyMemberDef Noddy_members[] = { {"first", T_OBJECT_EX, offsetof(Noddy, first), 0, "first name"}, {"last", T_OBJECT_EX, offsetof(Noddy, last), 0, "last name"}, {"number", T_INT, offsetof(Noddy, number), 0, "noddy number"}, {NULL} /* Sentinel */ };
Ezt a tp_members mezőbe jegyezzük fel a már ismert módon.

Minden tagfüggvénynek van neve, típusa, címe, elérhetőségi bitjei, és dokumentációs sztringje. További részletekért a dokumentáció "generic attribute management" részét kell elolvasni.

A példában leírt módszernek az a hátránya, hogy nem tudjuk megszorítani azon típusok körét, akik hozzáférhetnek mindezen attribútumokhoz. Szándékunk szerint például a first és a last mezők neveket tartalmaznának, de egyelőre bármilyen Python objektum értékül adható nekik. Sőt, akár törölhetőek, egyszerűen nullmutató értéket kell nekik adni.

Ha a fenti példában megnézzük a name függvényt, ott erre az esetre fel is készültünk. Ez a függvény viszont inkább azért érdekes, mert nem csak adattagokat definiáltunk, hanem ezt a függvényt is, mint az új típusunk függvényét.

static PyMethodDef Noddy_methods[] = { {"name", (PyCFunction)Noddy_name, METH_NOARGS, "Return the name, combining the first and last name" }, {NULL} /* Sentinel */ };
A metódustömböt létrehozó függvényt a tp_methods mezőbe jegyeztük be. Azzal, hogy a METH_NOARGS konstanst használtuk, jeleztük, hogy a függvény nem fogad paramétereket.

Végül a tp_flags mezőbe beírt Py_TPFLAGS_BASETYPE értékkel engedélyeztük az új típusunk bázisosztályként való felhasználását.

Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
A második példát így lehet felhasználni:
from distutils.core import setup, Extension setup(name="noddy", version="1.0", ext_modules=[ Extension("noddy", ["noddy.c"]), Extension("noddy2", ["noddy2.c"]), ])
Ahol a noddy2 a második példa, és noddy2.c a fájl neve, ahol definiáltuk.

Ahhoz, hogy az adattagjainkat megvédjük az ártalmas értékadásoktól, érdemes saját értékadó függvényt írni hozzájuk, és persze lekérdezőfüggvényt is. Mint például:

Noddy_setfirst(Noddy *self, PyObject *value, void *closure) { if (value == NULL) { PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute"); return -1; } if (! PyString_Check(value)) { PyErr_SetString(PyExc_TypeError, "The first attribute value must be a string"); return -1; } Py_DECREF(self->first); Py_INCREF(value); self->first = value; return 0; }
A closure nevű paramétert itt nem használjuk ki, lehetőség van például arra, hogy csak egy beállító és lekérdező műveletet írunk meg, ami a paraméterből tudja meg, hogy melyik attribútumról van szó.

A típusműveletekről

A PyTypeObject osztálynak igen sok metódusa van, íme:

typedef struct _typeobject { PyObject_VAR_HEAD char *tp_name; /* For printing, in format "." */ int tp_basicsize, tp_itemsize; /* For allocation */ /* Methods to implement standard operations */ destructor tp_dealloc; printfunc tp_print; getattrfunc tp_getattr; setattrfunc tp_setattr; cmpfunc tp_compare; reprfunc tp_repr; /* Method suites for standard classes */ PyNumberMethods *tp_as_number; PySequenceMethods *tp_as_sequence; PyMappingMethods *tp_as_mapping; /* More standard operations (here for binary compatibility) */ hashfunc tp_hash; ternaryfunc tp_call; reprfunc tp_str; getattrofunc tp_getattro; setattrofunc tp_setattro; /* Functions to access object as input/output buffer */ PyBufferProcs *tp_as_buffer; /* Flags to define presence of optional/expanded features */ long tp_flags; char *tp_doc; /* Documentation string */ /* Assigned meaning in release 2.0 */ /* call function for all accessible objects */ traverseproc tp_traverse; /* delete references to contained objects */ inquiry tp_clear; /* Assigned meaning in release 2.1 */ /* rich comparisons */ richcmpfunc tp_richcompare; /* weak reference enabler */ long tp_weaklistoffset; /* Added in release 2.2 */ /* Iterators */ getiterfunc tp_iter; iternextfunc tp_iternext; /* Attribute descriptor and subclassing stuff */ struct PyMethodDef *tp_methods; struct PyMemberDef *tp_members; struct PyGetSetDef *tp_getset; struct _typeobject *tp_base; PyObject *tp_dict; descrgetfunc tp_descr_get; descrsetfunc tp_descr_set; long tp_dictoffset; initproc tp_init; allocfunc tp_alloc; newfunc tp_new; freefunc tp_free; /* Low-level free-memory routine */ inquiry tp_is_gc; /* For PyObject_IS_GC */ PyObject *tp_bases; PyObject *tp_mro; /* method resolution order */ PyObject *tp_cache; PyObject *tp_subclasses; PyObject *tp_weaklist; } PyTypeObject;
Ezek, noha sokan vannak, nem jelentenek implementációs akadályt, mert általában az alapértelmezett működésük jól használható, valójában kevés, speciális működést igénylő esetekben kell implementálni ilyen metódust.

Néhány szó ezekről a metódusokról.

A tp_dealloc a destruktor, akkor hívódik meg, ha a referenciaszámláló eléri a nullát, és a Python interpreter fel akarja szabadítani a területet. Itt kell implementálni a típus által lefoglalt memóriát (tehát a mutatóként használt mezők tartalmát, nem a típus attribútumait) felszabadító eljárást. Ügyelni kell a kivételkezelésre is, mert a tp_dealloc nincs tekintettel az érvényes kivételekre, és így a hiba eredeti helyét elrejtheti (nem lehet eldönteni, hogy a tp_dealloc idézi-e elő a kivételkiváltódást, vagy még valami korábban)

A tp_repr, tp_str, tp_print metódusok az objektum reprezentációi, az automatikus mentés, és általában a perzisztencia eszközei. Működésének az alapja, hogy a tp_name mezők egyértelmű leképezést biztosítanak, és mivel minden objektumnak van neve, a hierarchikus reprezentáció egyértelmű. Mindemellett hibakereséshez kiválóak ezek a függvények, a tp_print kifejezetten olvasható kimenetet ad.

A tp_getattr, tp_setattr, és a tp_get/tp_setattrofunc az attribútumok elérésére valók.

Az attribútumoknak módosítókat lehet adni, mint READONLY vagy RESTRICTED, ami a hozzáférési lehetőségeket állítja.

A tp_compare összehasonlít két objektumot a típusból. Visszatérési értéke -1, 0, 1 lehet, attól függően, hogy kisebb, egyenlő vagy nagyobb volt az első paraméter a másodiknál.

A Python sok helyen használ absztrakt típusokat, ezeket mint felületeket sok helyen láthatunk a típusmetódusok között, például a PyNumberMethods tp_as_number az egészekkel végezhető műveleteket tartalmazza. Amint az látható, igen sok ilyen felületet ölel fel a típusmetódusok köre, megadva ezzel a lehetőséget új típusunknak, hogy igen sok függvényt definiáljon felül, például így használhatunk iterátorokat is.

Amiről még írni kellene