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 :-).
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.
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:
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:
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.
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.
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:
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:
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.
Egyenlőre kihagyva a hibaellenőzést:
Az előbbiek fényében már érthető a
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:
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:
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.
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.
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
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:
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:
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.
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é.
Erre a feladatra a
A formátum string leírását nézzétek meg a dokumentácóban.
Példák:
TODO :-)
Erre a feladatra a
Példák:
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ó.
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.
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).
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:
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:
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:
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.
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).
A spam_system függvényt triviálisan módosítottuk csak:
A modul elejére közvetlenül a
A munka nagyrésze a spammodule.h fájlba kerül, ami így néz ki:
Ezek után a kliens dolga igen egyszerű:
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
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.
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.
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:
Hasonlításképpen nézzük a Python szabványos egész típusának megfelelő definícióját:
A következő sornál:
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:
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.
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.
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.
Minden más a kódban ismerős, kivéve azt a kis szakaszt az initnoddy() függvényben:
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:
Nézzük a következő kódrészletet:
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:
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:
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:
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:
Sokféleképpen lehet elérhetővé tenni ezeket az attribútumokat, a legegyszerűbb mód, hogy tagdefiníciókat írunk:
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.
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.
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:
A PyTypeObject osztálynak igen sok metódusa van, íme:
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.