9. Osztályok

Más nyelvekhez viszonyítva a Python osztálymechanizmusa a nyelvhez a lehető legkevesebb új szintaxissal és szemantikával ad hozzá osztályokat. A Python osztályok a C++–ban és a Modula-3–ban található osztálymechanizmusok keveréke. A Python osztályok rendelkeznek az objektumorientáltság minden jellemzőjével: az öröklődési mechanizmus lehetővé teszi a több őstől való származtatást; a származtatott osztályok a szülők bármely metódusát felül tudják írni; a metódusok ugyanazon a néven érhetik el a szülőosztály metódusait. Az objektumok tetszőleges számú és típusú adatot tartalmazhatnak. Ahogy a modulok, úgy az osztályok is részt vesznek a Python dinamikus természetében: futás közben létrehozhatóak, és tovább módosíthatóak létrehozásuk után.

C++ szóhasználattal élve az osztály minden eleme (beleértve az adattagokat is) publikus (a kívételt lásd lejjebb Privát változók), és minden tagfüggvény virtuális. A Modula-3–hoz hasonlóan nincsen rövidített hivatkozás az objektum alkotóelemeire annak metódusaiból: az objektum függvényeinek deklarálásakor első argumentumként az objektumot jelképező változót adjuk meg, mely híváskor automatikusan átadásra kerül. A Smalltalk-hoz hasonlóan az osztályok önmaguk is objektumok. Ez teremt szemantikát az importáláshoz és átnevezéshez. A C++–tól és a Modula-3-tól eltérően a beépített típusok szülőosztályokként felhasználhatók. S végül a C++-hoz hasonlóan legtöbb, egyéni szintaktikával bíró beépített operátor (aritmetikai műveletek, indexelés stb.) újradefiniálhatók az osztály példányaiban.

(Nem lévén általánosan elfogadott szóhasználat az osztályok témakörére, alkalmanként Smalltalk és C++ kifejezéseket fogok használni. (Szerettem volna Modula-3 kifejezéseket alkalmazni, mert annak jobban hasonlít az objektum-orientáció-szemlélete a Pythonhoz, mint a C++-é, de sejtésem szerint kevés olvasó hallott erről a nyelvről.)

9.1. Pár szó a nevekről és objektumokról

Az objektumoknak egyéni jellege van, és több nevet lehet kapcsolni ugyanahhoz az objektumhoz (akár különböző névterekben). Ez a lehetőség fedőnévhasználatként (aliasing) ismert más nyelvekben. Ezt a lehetőséget a nyelvvel való első találkozáskor rendszerint nem becsülik meg, és nyugodtan mellőzhető megváltoztathatatlan típusok használatakor (például számok, karakterláncok, tuple-ok esetében). Valójában a fedőnév használata valószínűleg meglepő módon viselkedik megváltoztatható típusok esetében, mint például a listák, szótárak, és a legtöbb más típus esetén. A fedőnevek általában a program hasznára válnak, mivel a mutatókhoz hasonlítanak néhány vonatkozásban. Például egy objektum átadása kevés erőforrásfelhasználással jár, mivel ilyenkor csak egy mutató fog mutatni az objektumra. Ha a meghívott függvény módosítja a neki átadott objektumot, a hívó látja a változást – ez szükségtelenné teszi két különböző argumentumátadási módszer használatát, mint amilyen a Pascalnál van.

9.2. Hatókörök és névterek a Pythonban

Mielőtt megismerkednénk az osztályokkal, beszélnünk kell a hatókörök Pythonbeli szabályairól. Az osztálydefiníciók néhány ügyesen trükköznek a névterekkel, és ismerned kell a névterek és hatókörök működését ahhoz, hogy teljesen átlásd, mi is történik. Egyébként ennek a témakörnek az ismerete minden haladó Python programozónak a hasznára válik.

Kezdetnek nézzünk meg néhány definíciót.

A névtér a nevekhez objektumokat rendel. A legtöbb névtér jelenleg Python szótárakként van megvalósítva, de ez (a teljesítmény kivételével) normális esetben nem észlelhető, és ez a jövőben változhat. Példák a névterekre: a beápített nevek listája (például az abs() függvény, vagy a beépített kivételek nevei); a modulokban jelenlévő globális nevek; vagy a helyi nevek függvényhívások során. Bizonyos értelemben egy objektum jellemzői is külön névteret alkotnak. Fontos tudni, hogy különböző névterekben lévő két név között semmilyen kapcsolat nem létezik. Például ha két különböző modul egyaránt definiálhat egy “maximumalizal” nevű függvényt bármilyen keveredés nélkül, mert a modulok használóinak a függvénynév előtagjában a modul nevével egyértelműen jelezniük kell, hogy pontosan melyik függvényt fogják használni.

Egyébként a jellemző szót használom bármilyen névre, ami pontot követ — például a z.real kifejezésben a real a z objektum egyik jellemzője. Szigorúan véve egy modulbeli névre való hivatkozás egy jellemző-hivatkozás: a modulnev.fuggvenynev kifejezésben a modulnev egy modul objektum és a fuggvenynev annak egy jellemzője. Ebben az esetben közvetlen hozzárendelés történik a modul jellemzői és a modulban definiált globális nevek között: ezek ugyanazon a névtéren osztoznak. [1]

A jellemzők lehetnek csak olvashatóak, vagy írhatóak is. Az utóbbi esetben értéket rendelhetünk a jellemzőhöz. A moduljellemzők írhatóak, helyes a következő utasítás: modulnev.vegso_valasz = 42. Az írható jellemzők a del utasítással törölhetők is. Például a del modulnev.vegso_valasz törli a a modulnev objektum vegso_valasz jellemzőjét.

A névterek különböző időpontokban születnek, és élettartamuk is változó. Az a névtér, amely a Python értelmező beépített neveit tartalmazza, a Python-értelmező indulásakor jön létre, és nem törölhető. A modulok globális névtere a moduldefiníció olvasásakor jön létre; általános esetben a modul névterek az értelmezőből való kilépésig megmaradnak. Az utasításokat, amelyet az értelmező felső szintje futtat le, vagy egy szkriptfájlból kiolvasva, vagy interaktív módon, azokat __main__ modul részének tekinti a Python, ezért saját névtérrel rendelkeznek. (A beépített nevek szintén egy modulban léteznek, builtins név alatt.)

A függvények helyi névtere a függvény hívásakor keletkezik, és a függvény lefutásakor, vagy a függvényben le nem kezelt kivételek létrejöttekor szűnnek meg. (Talán a felejtés szó pontosabb kifejezés lenne arra, ami történik.) Természetesen a rekurzív hívások mindegyike saját, helyi névtérrel rendelkezik.

A hatókör (angolul scope) a Python kód azon szöveges része ahol a névtér közvetlenül elérhető. A közvetlen elérhetőség itt azt jelenti, hogy a név a teljes elérési útjának kifejtése nélkül elérhető a névtérben. (például a z.real-ben a . jelzi, hogy a z objektumhoz tartozó jellemzőről van szó, ez itt most teljesen kifejtett.)

Ámbár a névterek meghatározása statikus, dinamikusan használjuk őket. Bármikor a program futása során legalább három egymásba ágyazott névtér létezik, amely közvetlenül elérhető:

  • a legbelső hatókör, amelyben először keresünk, a helyi neveket tartalmazza
  • minden bezáró függvény hatóköre, amelyben a keresést a legközelebbi bezáró hatókörrel kezdjük, tartalmaz nem-helyi, de nem-globális neveket is
  • at utolsó előttiként vizsgált hatáskör az aktuális modul globális neveit tartalmazza
  • a legkülső (utolsónak vizsgált) hatókör a beépített neveket tartalmazó névtér

Ha egy változónevet globálisnak deklaráltunk, minden hivatkozás és értékadás közvetlenül a középső hatókörbe megy, abba, amelyben a modul globális nevei találhatóak. A legbelső névtéren kívül található változók újracsatolásához a nonlocal utasítás használható; ha nem deklaráltuk nemlokálisnak, azok a változók csak olvashatóak (minden kísérlet, hogy ilyen változóba írjunk egyszerűen új változót hoz létre a legbelső névtérben, változatlanul hagyva az azonos nevű külső változót).

Rendszerint a helyi hatókör a szövegkörnyezetben található helyi változókra hivatkozik az aktuális függvényben. A függvényeken kívül a helyi hatókör megegyezik a globális hatókörrel: a modul névtere. Az osztálydefiníciók pedig még egy újabb névtereket helyeznek el a helyi hatókörben.

Fontos tudatosítani, hogy a hatókörök szövegkörnyezet által meghatározottak: a modulban definiált függvény globális hatóköre a modul névtere, nem számít, hogy hol és milyen fedőnévvel hívjuk meg a függvényt. Másrészről az aktuális nevek keresése dinamikusan, futásidőben történik, – a nyelvi definíció akármennyire is törekszik a fordításkori, statikus névfeloldásra, szóval hosszútávon ne számíts a dinamikus névfeloldásra! (Igazság szerint a helyi változók mindig statikusan meghatározottak.)

A Python egy különleges tulajdonsága, hogy – global utasítás nincs érvényben – a névhez hozzárendelés mindig a belső névtérben történik. A hozzárendelés nem másol adatokat — csak kötést hoz létre a nevek és az objektumok között. A törlésre ugyanez igaz: a del x utasítás eltávolítja az x kötését a helyi névtér nyilvántartásából. Valójában minden művelet, amely új nevet vezet be, a helyi hatókört használja: nevezetesen az import utasítás és a függvénydefiníció a helyi hatókörhöz kapcsolja a modul vagy a függvény nevét.

A global kulcsszóval jelezheted hogy bizonyos változók a globális névtérben léteznek és ott újra kell csatolni, a nonlocal kulcsszó azt jelzi, hogy a szóban forgó változó a helyi hatókörhöz tartozik, és ott kell újracsatolni.

9.2.1. Hatókör és névtér példa

Itt van egy példa, amely bemutatja, hogyan lehet hivatkozni a különböző hatókörökre és névterekre, és hogyan hat a global és nonlocal a változócsatolásokra:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

A példakód kimenete:

.. code-block:: none
After local assignment: test spam After nonlocal assignment: nonlocal spam After global assignment: nonlocal spam In global scope: global spam

Jegyezd meg, hogy a helyi értékadás (amely az alapértelmezett) nem változtatja meg a scope_test függvény spam csatolását. A nonlocal értékadás a scope_test spam nevének csatolását változtatja meg, a global értékadás pedig a modulszintű csatolást.

Azt is érdemes észrevenni, hogy nem csatoltunk értéket a spam-hez a global értékadás előtt.

9.3. Első találkozás az osztályokkal

Az osztályok használatához szükségünk van új szintaxisra: három új objektumtípusra, és némi új szemantikára.

9.3.1. Az osztálydefiníció szinaxisa

A legegyszerűbb osztálydefiníció így néz ki:

class OsztalyNev:
    <utasitas-1>
    .
    .
    .
    <utasitas-N>

Az osztálydefiníciók hasonlítanak a függvények definíciójára (def statements) abból a szempontból, hogy az osztály deklarációjának meg kell előznie az első használatot. (Osztálydefiníciót elhelyzehetsz egy if utasítás valamely ágában is, vagy egy függvénybe beágyazva.)

A gyakorlatban az osztályokon belüli utasítások többsége általában függvénydefiníció, de bármilyen más utasítás is megengedett, és néha hasznos is – erre még később visszatérünk. Az osztályon belüli függvényeknek normál esetben egyedi argumentumlistájuk (és hívási módjuk) van az osztály metódusainak hívására vontakozó megállapodás szerint – ezt szintén később fogjuk megvizsgálni.

Egy osztálydefinícióba való belépéskor új névtér jön létre és válik a helyi hatókörré – ebből kifolyólag minden helyi változóra történő hivatkozás ebbe az új névtérbe kerül. A gyakorlatban általában az új függvények csatolásai kerülnek ide.

Az osztálydefiníciókból való normális kilépéskor (amikor elérünk a végéhez) egy osztályobjektum jön lére. Ez lényegében egybefoglalja, beburkolja az osztálydefiníciókor létrejött új névtér tartalmát – az osztályobjektumokról a következő alfejezetben fogunk többet tanulni. Az eredeti helyi névtér (az osztálydefinícióba való belépés előtti állapotában) helyreállítódik, és az osztályobjektum neve is a helyi névtér része lesz (az OsztalyNev a példában).

9.3.2. Osztályobjektumok

Az osztályobjektumok a műveletek kétféle típusát támogatják: a jellemzőhivatkozást és a példányosítás. A jellemzőhivatkozások az általánosan használt Python jelölésmódot használják: objektum.jellemzőnév. Az összes név érvényes jellemzőnév ami az osztály névterében volt az osztályobjektum létrehozásakor. Ha egy osztály definíció valahogy így néz ki:

class Osztalyom:
    "Egy egyszerű példa osztály"
    i = 12345
    def f(self):
        return 'hello világ'

akkor Osztalyom.i és Osztályom.f egyaránt érvényes jellemzőhivatkozás – egy egész számmal illetve egy függvényobjektummal térnek vissza. Az osztályjellemzőknek ugyanúgy adhatunk értéket mint egy normális változónak (Osztalyom.i = 2). A __doc__ metódus is érvényes attribútum, ami az osztály dokumentációs karakterláncával tér vissza: "Egy egyszerű példa osztály"

Egy osztály példányosítása a függvények jelölésmódját használja. Egyszerűen úgy kell tenni, mintha az osztályobjektum egy argumentum nélküli függvény lenne, amit meghívva az osztály egy új példányát kapjuk visszatérési értékként. Például (a fenti osztályt alapul véve):

x = Osztalyom()

létrehoz egy új példányt az osztályból, és hozzárendeli a visszatérési értékként kapott objektumot az x helyi változóhoz.

A példányosítás művelete (az objektum ,,hívása’‘) egy üres objektumot hoz létre. Elképzelhető, hogy az új példányt egy ismert kezdeti állapotba állítva szeretnénk létrehozni. Ezt egy különleges metódussal, az __init__()-el tudjuk elérni:

def __init__(self):
    self.data = []

Ha az osztály definiálja az __init__() metódust, egy új egyed létrehozásakor annak __init__() metódusa automatikusan lefut. Lássunk egy példát egy új, inicializált egyedre:

x = Osztalyom()

Természetesen az __init__() metódusnak argumentumokat is átadhatunk a nagyobb rugalmasság kedvéért. Az argumentumok az osztály példányosítása során az inicializáló metódushoz jutnak. Lássunk egy példát:

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. A létrehozott egyedek

És most mihez tudunk kezdeni a példányobjektumokkal? A példányobjektumok csak a jellemzőhivatkozás műveletet ismerik. Két lehetséges jellemzőnév van: adatjellemzők és metódusok.

A adatjellemzők fogalma megegyezik a Smalltalk ,,példányváltozók’’ fogalmával, és a C++ ,,adattagok’’ fogalmával. Az adatjellemzőket nem kell a használatuk előtt deklarálni, a helyi változókhoz hasonlatosan működnek – az első használatukkor automatikusan létrejönnek. Például ha x az Osztalyom egy példánya, a következő kódrészlet 16-ot fog kiírni:

x.szamlalo = 1
while x.szamlalo < 10:
    x.szamlalo = x.szamlalo * 2
print(x.szamlalo)
del x.szamlalo

A másik fajta jellemző a metódus (más néven tagfüggvény). A metódus egy objektumhoz ,,tartozó’’ függvényt jelöl. (A Pythonban a metódus kifejezés nem kizárólag egy osztály példányának metódusát jelenti – más objektum típusok is rendelkezhetnek metódusokkal. Például a listaobjektumoknak vannak saját metódusai: append, insert, remove, sort, és így tovább. Az alábbi sorokban a metódus kifejezést kizárólag egy osztály metódusaira értjük, hacsak nincs külön kihangsúlyozva, hogy most egy másik objektum metódusáról van szó.

A létrehozott objektum metódusainak neve az osztályától függ. Meghatározás szerint minden felhasználó által definiált metódust az adott (létező) példány nevével kell hívni. Például x.f egy érvényes függvényhivatkozás, ha az Osztalyom.f függvény létezik (x objektum az Osztalyom példánya), de x.i nem érvényes ha Osztalyom.i változót nem hoztuk létre az osztály definiálásakor. Fontos, hogy x.f nem ugyanaz, mint Osztalyom.f — ez egy metódusobjektum, nem egy függvényobjektum.

9.3.4. Az metódusobjektumok

Többnyire a metódusokat rögtön meghívjuk:

x.f()

Példánkban x.f a 'hello világ' string-el tér vissza. Ezt a függvényt nem csak közvetlenül hívhatjuk meg: x.f egy objektum metódus, tárolható és később is hívható, például így:

xf = x.f
while True:
    print(xf())

Ez a kód az örökkévalóságig a hello világ üzenetet írja ki.

Pontosan mi történik egy objektummetódus hívásakor? Lehet, hogy már észrevetted hogy a x.f()-t a fenti példában argumentum nélkül hívtuk meg - annak ellenére, hogy f() függvénydefiníciója egy argumentum használatát előírja. Mi van ezzel a argumentummal? Szerencsére a Pythonban ha egy argumentumot igénylő függvényt argumentum nélkül próbálunk meghívni, kivételdobás történik.

Lehet hogy már kitaláltad a választ: az a különleges a metódusokban, hogy hívásukkor az őket tartalmazó osztálypéldányt megkapják az első változóban. A példánkban x.f() hívása pontosan ugyanaz, mintha Osztalyom.f(x) metódust hívnánk. Általában metódusok hívása n argumentummal ugyanaz, mintha az osztálydefiníció függvényét hívnánk meg úgy, hogy a legelső argumentum elé az aktuális példány nevét beillesztjük.

Ha nem értenél valamit a metódusok működéséről, nézz meg kérlek néhány gyakorlati példát. Amikor egy példányjellemzőjére hivatkozol, és az nem létezik a változók között, az értelmező az osztálydefinícióban fogja keresni. Ha a név egy érvényes osztályjellemzőre mutat, ami egy függvény, a fenti példában szereplő folyamat történik: az értelmező az x.f() hívást átalakítja – az argumentumokat kigyűjti, majd első argumentumként x-et tartalmazva létrehoz egy új argumentumlistát és meghívja a Osztalyom.f(x, argumentum1, arg2...) függvényt.

9.4. Mindenféle megjegyzés

Az adatjellemzők felülírják az ugyanolyan nevű metódusokat; a névütközések elkerülése végett (amelyek nagyon nehezen megtalálható programhibákhoz vezethetnek) érdemes betartani néhány elnevezési szabályt, melyekkel minimalizálható az ütközések esélye. Ezek a szabályok például a metódusok nagybetűvel írását, az adatjellemzők kisbetűs írását – vagy alsóvonás karakterrel kezdését jelentik; vagy igék használatát a metódusokhoz, és főnevekét az adatjellemzőkhöz.

Az adatjellemzőkre a metódusok is hivatkozhatnak, éppúgy mint az objektum hagyományos felhasználói. Más szavakkal az osztályok nem használhatók csupasz absztrakt adattípusok megvalósítására. Valójában a Pythonban jelenleg semmi sincs, ami az adatrejtés elvét biztosítani tudná – minden az elnevezési konvenciókra épül. Másrészről az eredeti C alapú Python képes teljesen elrejteni a megvalósítási részleteket és ellenőrizni az objektum elérését, ha szükséges; ehhez egy C nyelven írt kiegészítést kell használni.

A kliensek az adatjellemzőket csak óvatosan használhatják, mert elronthatják azokat a variánsokat, amelyeket olyan eljárások tartanak karban, amelyek időpontbélyeggel dolgoznak. Az objektum felhasználói saját adatjellemzőiket bármiféle ellenőrzés nélkül hozzáadhatják az objektumokhoz amíg ezzel nem okoznak névütközést – az elnevezési konvenciók használatával elég sok fejfájástól megszabadulhatunk!

A metódusokban nem használhatunk rövidítést az adatjellemzőkre (vagy más metódusokra). Én úgy látom, hogy ez növeli a metódusok olvashatóságát, és nem hagy esélyt a helyi és a példányosított változók összekeverésére, mikor a metódus forráskódját olvassuk.

A hagyományokhoz hűen a metódusok első argumentumának neve rendszerint self. Ez valóban csak egy szokás: a self névnek semmilyen speciális jelentése nincs a Pythonban. Azért vegyük figyelembe, hogy ha eltérünk a hagyományoktól, akkor a program nehezebben olvashatóvá válik, és a osztályböngésző is a tradicionális változónevet használja.

Az osztály definíciójában megadott függvények az osztály példányai számára hoznak létre metódusokat (a példányhoz tartozó függvényeket). Nem szükségszerű azonban hogy egy függvénydefiníció kódja az osztálydefiníció része legyen: egy definíción kívüli függvény helyi változóhoz való rendelése is megfelel a célnak. Például:

# Egy osztályon kívül definiált függvény
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1
    def g(self):
        return 'hello világ'
    h = g

Most f, g és h egyaránt C osztály jellemzői (gyakorlatilag objektumhivatkozások) – következésképpen C osztály minden példányának metódusai is – h és g pedig valójában ugyanazt a függvényt jelentik. Azért ne feledjük, hogy a fenti példa használata a program olvasóját összekavarhatja!

Az osztályon belüli metódusok egymást is hívhatják a self argumentum használatával:

class Taska:
    def __init__(self):
        self.adat = []
    def belerak(self, x):
        self.adat.append(x)
    def belerak_ketszer(self, x):
        self.belerak(x)
        self.belerak(x)

A metódusok a globális névtérben lévő függvényekre is hasonlóképp hivatkozhatnak. (Maguk az osztálydefiníciók soha nem részei a globális névtérnek!) Míg egy kivételes esetben a globális névtér változóinak használata jól jöhet, több esetben is jól jöhet a globális névtér elérése: a globális névtérbe importált függvényeket és modulokat az adott osztálymetódusból is használhatjuk, mintha az adott függvényben vagy osztályban definiálták volna azokat. Rendszerint az osztály az önmaga által definiált metódust a globális névtérben tartja, és a következő részben meglátjuk majd, miért jó ha a metódusok a saját osztályukra hivatkozhatnak!

9.5. Öröklés

Természetesen az öröklés támogatása nélkül nem sok értelme lenne az osztályok használatának. A származtatott osztályok definíciója a következőképpen néz ki:

class SzarmaztatottOsztalyNeve(SzuloOsztalyNeve):
    <utasitas-1>
    .
    .
    .
    <utasitas-N>

A SzuloOsztalyNeve névnek abban a névtérben kell lennie, ahol a származtatott osztályt definiáljuk. A szülőosztály neve helyett más tetszőleges kifejezés is megengedett. Ez akkor hasznos, ha az szülőosztály definíciója másik osztályban van:

class SzarmaztatottOsztalyNeve(modulnev.SzuloOsztalyNeve):

A származtatott osztály definíciójának feldolgozása hasonló a szülőosztályokéhoz. Az osztályobjektum a létrehozásakor megjegyzi a szülőosztályt. Ezt használjuk arra, hogy feloldjuk a jellemzőkre történő hivatkozásokat: ha a keresett jellemző nincs jelen az osztályban, a keresés a szülőosztályban folytatódik. Ez a szabályt alkalmazza a Python rekurzívan, hogyha a szülőosztály maga is származtatott osztálya egy másik osztálynak.

A származtatott osztályok példányosításában nincs semmi különleges: SzarmaztatottOsztalyNeve() létrehozza az osztály új példányát. A metódus-hivatkozások feloldása a következőképpen történik: a megfelelő osztály jellemzőjét keresi meg, ha szükséges végigkutatva a szülőosztályok láncát, és ha a talált jellemző egy függvény, akkor a metódus-hivatkozás érvényes.

A származtatott osztályok felülírhatják a szülőosztályok metódusait. Mivel a metódusoknak nincsenek különleges előjogaik, amikor ugyanannak az objektumnak más metódusát hívják, ezért a szülőosztályban definiált egyik metódus, amely egy szintén a szülőosztályban definiált másik metódust hívná eredetileg, lehet, hogy a származtatott osztály által felülírt metódust fogja meghívni. (C++ programozóknak: a Pythonban lényegében minden metódus virtuális.)

A származtatott osztály metódusa, amely felülírja a szülőosztály egy metódusát, valójában inkább kiterjeszti az eredeti metódust, és nem egyszerűen csak kicseréli. A szülőosztály metódusára így hivatkozhatunk: SzuloOsztalyNev.metodusnev(self, argumentumok). Ez néha jól jöhet. (Fontos, hogy ez csak akkor működik, ha a szülőosztály a globális névtérben lett létrehozva, vagy közvetlenül beimportálva.)

9.5.1. Többszörös öröklés

A Python támogatja a többszörös öröklést egy formáját is. Egy több szülőosztályból származtatott osztály definíciója a következőképp néz ki:

class SzarmaztatottOsztaly(Szulo1, Szulo2, Szulo3):
    <utasitas-1>
    .
    .
    .
    <utasitas-N>

Az egyedüli nyelvtani szabály amit ismerni kell, az osztályjellemzők feloldásának a szabálya. Az értelmező először a mélyebb rétegekben keres, balról jobbra. A fenti példában ha a jellemző nem található meg a SzarmaztatottOsztaly-ban, akkor először a Szulo1-ben keresi azt, majd rekurzívan a Szulo2-ben, és ha ott nem találja, akkor lép tovább rekurzívan a többi szülőosztály felé.

Néhányan első pillanatban arra gondolnak, hogy a Szulo2-ben és a Szulo3-ban kellene előbb keresni, a Szulo1 előtt — mondván hogy ez természetesebb lenne. Ez az elgondolás viszont igényli annak ismeretét, hogy mely jellemzőt definiáltak a Szulo1-ben vagy annak egyik szülőosztályában, és csak ezután tudod elkerülni a Szulo2-ben lévő névütközéseket. A mélyebb először szabály nem tesz különbséget a helyben definiált és öröklött változók között.

Ezekből gondolom már látszik, hogy az átgondolatlanul használt többszörös öröklődés a program karbantartását rémálommá teheti – a névütközések elkerülése végett pedig a Python csak a konvenciókra támaszkodhat. A többszörös öröklés egyik jól ismert problémája ha a gyermekosztály két szülőosztályának egy közös nagyszülő osztálya van. Ugyan egyszerű kitalálni hogy mi történik ebben az esetben (a nagyszülő adat jellemzőinek egypéldányos változatát használja a gyermek) – az még nem tisztázott, hogy ez a nyelvi kifejezésmód minden esetben használható-e.

További részletekért lásd https://www.python.org/download/releases/2.3/mro/.

9.6. Privát változók

Egyedi azonosítók létrehozását az osztályokhoz a Python korlátozottan támogatja. Bármely azonosító, amely így néz ki: __spam (legalább két bevezető alsóvonás, amit legfeljebb egy alsóvonás követhet) szövegesen kicserélődik a _classname__spam formára, ahol a classname az aktuális osztály neve egy alsóvonással bevezetve. Ez a csere végrehajtódik az azonosító pozíciójára való tekintet nélkül, úgyhogy használható osztály-egyedi példányok, osztályváltozók, metódusok definiálására — még akkor is, ha más osztályok példányait saját privát változói közé veszi fel (???Ford: ellenőrizni!)

Ha a cserélt név hosszabb mint 255 karakter, az értelmező csonkíthatja az új nevet. Külső osztályoknál, vagy ahol az osztálynév következetesen alsóvonásokból áll??? nem történik csonkítás. A névcsonkítás célja az, hogy az osztályoknak egyszerű megoldást biztosítson a “private” változók és metódusok definiálására — anélkül, hogy aggódnunk kellene a származtatott osztályokban megjelenő privát változók miatt, vagy esetleg a származtatott osztályban már meglévő változó privát változóval való felülírása miatt. Fontos tudni, hogy a névcsonkítási szabályok elsősorban a problémák elkerülését célozzák meg — így még mindig leheséges annak, aki nagyon akarja, hogy elérje vagy módosítsa a privátnak tartott változókat.

Ez speciális körülmények között nagyon hasznos lehet, például hibakeresésnél, és ez az egyik oka annak, hogy ezt a kibúvót még nem szüntették meg.

(Buglet (duda): származtatás egy osztályból a szülőosztály nevével megegyező néven – a szülőosztály privát változóinak használata ekkor lehetséges lesz.)

Fontos, hogy a exec, eval() vagy evalfile() által végrehajtott kód nem veszi figyelembe a hívó osztály nevét az aktuális osztály esetében — ez hasonló a global változók működéséhez, előre lefordított byte-kód esetében. Hasonló korlátozások léteznek a getattr(), setattr() és delattr() esetében, ha közvetlenül hívják meg a __dict__ utasítást.

9.7. Egyebek...

Alkalomadtán hasznos lehet a Pascal “record”, vagy a C “struct” adattípusaihoz hasonló szerkezetek használata – egybefogni néhány összetartozó adatot. A következő üres osztálydefiníció ezt szépen megvalósítja:

class Alkalmazott:
    pass

john = Alkalmazott()  # Egy üres alkalmazott rekordot hoz létre

# A rekord mezőinek feltöltése
john.nev = 'John Doe'
john.osztaly = 'számítógépes labor'
john.fizetes = 1000

Egy Python-kódrészletnek, amely valamilyen elvont adattípust vár, gyakran egy osztályt adunk át, amely megvalósítja annak az osztálynak a metódusait. Páltául, ha van egy függvényünk, amely a fájlobjektum némely adatát formázza, akkor definiálhatunk egy osztályt read() and readline() metódusokkal, amely az adatokat a karakterlánc-pufferből veszi inkább, és argumentumként átadja ezt.

A példánymetódus-objektumoknak is vannak jelemzőik: m.__self__ az m() metódussal rendelkező objektumpéldány, m.__func__ a metódushoz tartozó függvényobjektum.

9.8. Iterátorok

Valószínűleg már észrevetted, hogy a legtöbb tárolóobjektum bejárható a for ciklusutasítás használatával:

for elem in [1, 2, 3]:
    print(elem)
for elem in (1, 2, 3):
    print(elem)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for sor in open("myfile.txt"):
    print(sor, end='')

Az elérés ilyen módja tiszta, tömör és kényelmes. Az iterátorok használata áthatja és egységesíti a Pythont. A színfalak mögött a for utasítás meghívja a iter() függvényt a bejárandó tárolóra. Ez a függvény egy iterátor-objektummal tér vissza, amely definiálja a __next__() metódust, amellyel a tároló elemeit lehet elérni, egyszerre egy elemet. Ha nincs több elem, a __next__() metódus StopIteration kivételt dob, amely a for ciklust megszakítja. A next() beépített függvénnyel is meghívhatom a __next__() metódust, ahogy a következő példában látható:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)

Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    next(it)
StopIteration

A ciklusok működésébe bepillantva már könnyű saját iterátorprotokollt adni egy osztályhoz. Definiálni kell az __iter__() metódust, ami a __next__() függvény eredményeképp létrejövő objektummal tér vissza. Ha az osztály definiálja a __next__() metódust, akkor az __iter__() egyszerűen a self objektummal tér vissza:

>>> class Reverse:
    "Iterator for looping over a sequence backwards"
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

9.9. Generátorok

A generátorok egyszerű és hatékony eszközök iterátorok készítésére. A normális függvényekhez hasonló a felépítésük, de a yield utasítást használják ha adatot szeretnének visszaadni. A __next__() metódus minden hívásakor a generátor ott folytatja az adatok feldolgozását, ahol az előző hívásakor befejezte (emlékszik minden változó értékére, és hogy melyik utasítást hajtotta végre utoljára). Lássunk egy példát arra, hogy milyen egyszerű egy generátort készíteni:

>>> def reverse(data):
        for index in range(len(data)-1, -1, -1):
            yield data[index]

>>> for char in reverse('golf'):
        print(char)

f
l
o
g

Bármi, amit egy generátorral megtehetsz, osztály alapú bejárókkal is kivitelezhető – az előző részben leírtak szerint. Ami a generátorokat tömörré teszi, hogy a __iter__() és a __next__() metódusok automatikusan létrejönnek.

Egy másik fontos lehetőség hogy a helyi változók, és a végrehajtás állapota automatikusan tárolódik a generátor két futása között.

Az automatikus metóduslétrehozáson és programállapot-mentésen felül, amikor a generátor futása megszakad, ez az esemény automatikusan StopIteration kivételt vált ki. Egymással kombinálva ezek a nyelvi szolgáltatások egyszerűvé teszik az iterátorok készítését néhány függvény megírásának megfelelő – viszonylag kis erőfeszítéssel.

9.10. Generátorkifejezések

Néhány egyszerű generátort egyszerűen lehet kódolni egy olyan kifejezéssel, amely a listaértelmezés szintaktikáját használja, de kerek zárójellel a szögletes helyett. Ezeket a kifejezéseket arra a helyzetre tervezték, amikor a generátor közvetlenül egy függvényben található. A generátorkifejezés jóval tömörebb, de kevésbé rugalmasak, mint a teljes generátordefiníciók és általában jóval memóriakímélőbbek, mint a megfelelő listaértelmezések.

Példák:

>>> sum(i*i for i in range(10))                 # négyzetek összege
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # belsőszorzat
260

>>> from math import pi, sin
>>> sin_tabla = {x: sin(x*pi/180) for x in range(0, 91)}

>>> egyedi_szavak = set(szo  for sor in oldal  for szo in sor.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> adat = 'golf'
>>> list(adat[i] for i in range(len(adat)-1, -1, -1))
['f', 'l', 'o', 'g']

Lábjegyzet

[1]Except for one thing. Module objects have a secret read-only attribute called __dict__ which returns the dictionary used to implement the module’s namespace; the name __dict__ is an attribute but not a global name. Obviously, using this violates the abstraction of namespace implementation, and should be restricted to things like post-mortem debuggers.