Ma nagy témába kezdünk bele: az objektumorientált programozásba. Ez is egy úgynevezett programozási paradigma, úgy mint a procedurális programozás, amit a függvényekről szóló bejegyzésben ismertettem. Ahhoz hasonlóan, tegyük fel most is a legfontosabb kérdéseket:
Mi az az objektumorientált programozás?
Az objektumorientált programozás (továbbiakban OOP) lényege igen egyszerű: összezárjuk az adatot, és a rajta végezhető műveleteket (encapsulation). Sok objektummal találkoztunk már az ezt megelőző részekben, úgyhogy innen vennék egy példát.
Nézzünk egy list
-et:
array = [1, 2, 3, 4]
Ha hozzá akarunk adni egy elemet a tömbhöz akkor azt így tehettük meg:
array.append(5)
Ez után ha printeljük az array
-t akkor a [1, 2, 3, 4, 5]
jelenik meg a kimeneten. Tehát az .append()
metódussal az array
-ben található adaton végeztünk egy műveletet: hozzáadtunk egy elemet. Látható: az adat, és a rajta elvégzett művelet mind ott van az array
-ben.
Ez a gondolatmenet konkrétan annyira átjárja a Python programozási nyelvet, hogy a szintaxis elemein kívül minden amit csak le tudunk írni az valójából egy objektum.
Az OOP persze ennél sokkal több; több félévnyi előadást lehet belőlük tartani az egyetemen, és könyvtárakat lehet megtölteni a szakirodalommal. Mi most rövidre fogjuk a témát, mert a lényeg igazából egyáltalán nem bonyolult.
A szakirodalom és az egyetem meg foglalkozzon a speciális és tudományos esetekkel
Miért akarunk objektumorientáltan programozni?
Az egységbe zárás nyilvánvaló előnye tehát az, hogy kérdés nélkül tudjuk hogy milyen műveleteket végezhetünk az adaton, hisz ezek a műveletek ott vannak az adattal együtt. Például tudjuk, hogy a list
típusú objektumoknak nincs .keys()
metódusuk, mert az csak a dict
objektumoknak van.
Ez az összezárás növeli a forráskód hordozhatóságát is: egy objektumban rengeteg adat és metódus elhelyezhető. Ha a függvények törzsében a forráskód egy kis izolált környezetben futott, akkor ahhoz képest egy objektumban egy konkrét izolált világ létezik.
Másik fontos dolog, hogy az objektumok működését leíró osztályok képesek a rá hogy tulajdonságokat örököljenek (inheritance) más osztályoktól. Ezt mindig valamilyen állattal szokták szemléltetni, úgyhogy vegyünk egy macskát.
A macska egy emlős állat, így tehát minden igaz rá ami úgy általában az emlősökre. Ugyan így az ember is egy emlős, tehát ha van egy rendszerünk amiben macska és ember osztályok is léteznek, akkor érdemes ezeknek az emlősök tulajdonságait örökölniük. Ennek több gyakorlati haszna van:
- Csökkentjük a forráskód redundanciáját azzal, hogy a mindkét fajra jellemző tulajdonságokat egy ősosztályból származtatjuk, amit aztán a származtatott osztályok automatikusan meg is kapnak.
- Ahogy a természetben, úgy a programban is, mind a macska és az ember objektumok emlősökként is fognak viselkedni. Így tehát ha a programban amit egy emlős objektummal meg lehet tenni, azt egy macskával vagy egy emberrel is lehetséges.
Sokan azt magyarázzák, hogy az OOP szemlélet közel áll a természet működéséhez, és végülis ezt beláthatjuk a fenti példákon.
A Python és az OOP kapcsolata
Bár a tudtunkon kívül eddig is folyamatosan objektumokat hoztunk létre és léptünk velük kapcsolatba a forráskódban, ettől még nem programoztunk objektumorientáltan, csak objektumokat használtunk. A Python lehetőséget nyújt többféle paradigma szerinti programozásra: lehet írni a kódot csak úgy fentről-lefelé, ahogy tetszik; vagy függvényekben, és nemsokára osztályokban is. Ezek egymás mellett jól elférnek, a Python nem kényszerít rá semmilyen szemlélet használatára.
Az OOP annyira egy sikeres szemlélet lett az évek során, hogy léteznek olyan programozási nyelvek, amikben csak és kizárólag objektumorientáltan lehet fejleszteni. Ilyen a Java vagy a C#.
Ezt elolvasva úgy tűnhet, hogy az OOP az evolúció csúcsa, de minimum az a pont a programozásba, ahova minden fejlesztő megérkezik. Mint amikor nagykorú lesz az ember. Én ezt nem teljesen gondolom így, de tény hogy ma az OOP az alap amire az iparban szükség van.
Az első osztályunk Pythonban
Osztály? Nem objektumokról volt idáig szó?
Ahhoz hogy elkerüljük a félreértéseket be kell vezetnünk néhány új fogalmat, úgyhogy jól tessék figyelni!
Az objektumok működésének leírását egy osztályban (class) végezzük. A leírás alapján gyárthatunk egy halom különböző példányt (instance). Ezeket az élő példányok hívjuk objektumoknak is (objects). Egy objektum osztálya egyben az objektum típusa is. Ahogy a [1, 2, 3]
típusa list
, úgy a lenti példában a my_cat
típusa majd Cat
osztály lesz.
Éppen ezért, na meg a szóismétlések kerülése végett, a macska objektumokat hívom majd
Cat
osztályúaknak vagyCat
típusúaknak is.
Elég a pofázásból, lássuk az első osztályt!
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print('Meow')
my_cat = Cat('Narancs')
print(my_cat.name)
my_cat.meow()
Ezzel most nagyjából annyi új információt zúdítottam rátok, mintha 0-ról kezdenénk a kódolást.
Bonyolultnak tűnhet, de nem kell aggódni. Ezt a fenti kódot két részre szedhetjük: a class
alatti blokk az osztály definíciója. Az utolsó néhány sor a példányosítás (instantiation), és pár egyszerű interakció az objektummal.
Az osztály
Vegyük az osztály definíciót még egyszer:
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print('Meow')
Python nyelvben az osztályt a class
szóval kell kezdeni, amit az osztály neve követ. A példában a macskák leírására alkalmas osztályt Cat
-nek neveztem el.
Pythonban az osztályok nevét szokás szerint camel case stílusban írják.
Ez azt jelenti, hogy az osztály nagy kezdőbetűvel kezdődik, és ha több szó alkotja az elnevezést, akkor a szavak közt nincs elválasztás, hanem minden szó nagy kezdőbetűt kap és egybe írják az összeset.
Példával sokkal egyszerűbb:Cat
,PhoneNumber
,ValueError
.Csak hogy teljes legyen a kép: ha rövidítést használunk, és a rövidítéssel együtt négy vagy több nagybetű követné egymást, akkor csak a rövidítés első kezdőbetűje lesz nagy.
Tehát:XmlWriter
,StringIO
,HtmlReader
Az osztály definíció blokkjában végre ismerős kulcsszó látható, a def
. Azt is mondhatnánk, hogy ez egy függvény, de mivel az osztályon bellül van ezért őt metódusnak (method) hívjuk. Ettől még ugyan úgy visszatérhetnek bármilyen értékkel, mint a függvények.
A fenti példában tehát két metódus látható, az __init__()
és a meow()
.
Az __init__()
Az __init__()
név egy különleges metódust takar az osztályon belül: ez az úgynevezett konstruktor (constructor). Ez azt jelenti, hogy a metódus automatikusan fut akkor, amikor példányosítjuk az osztályt, azaz amikor egy új objektumot készítünk az osztály alapján. Ebben a metódusban lehet megadni, hogy a létrejövő objektum milyen kezdeti értékekkel rendelkezzen.
A konstruktorból nem lehet semmilyen értékkel se visszatérni.
Arra hogy a metódus pontosan mikor és hogyan fog lefutni kicsit később térünk vissza.
Pythonban azokat a metódusokat, amik
__
-el kezdődnek- és végződnek, magic method-nak hívják, és rengeteg létezik belőlük.
A self
Az __init__
két paramétert vár, a self
-et és a name
-et. Az osztályon belül a metódusok legelső paramétere, amit itt most self
-nek hívok, mindig különleges jelentőséggel bír: ez a paraméter impliciten (“magától”) értéket kap, méghozzá az objektumot magát, majd akkor, amikor példányosítottuk az osztályt. Tehát ha egyetlen paramétert se vár a metódusunk, a self
et akkor is ki kell írni, máskülönben hibát jelez a Python.
A paramétert szokás szerint
self
-nek hívják, de igazából akárhogy el lehet nevezni, viszont nagyon hülyén fognak rátok nézni ha nem így hívjátok.A valóságban van egy olyan eset, amikor a legelső paraméter mégsem képvisel különleges értéket, de erről majd később írok egyszer.
A self
arra való, hogy ezen keresztül tudják majd az objektum metódusai egymást és az adattagokat elérni.
Mivel a
self
mindig ott áll a metódus paramétersorában, ezért nem is szokás külön említeni. Így a jövőben nem azt fogom írni, hogy a konstruktor két paramétert vár, a selfet és a name-et hanem hogy a konstruktor egy paramétert vár, a name-et.
Ahogy tovább haladunk a self
mellett, ugyan azok a szabályok lesznek érvényesek, amik a közönséges függvények esetében is, úgyhogy ezen nem is agyalunk tovább.
A konstruktor törzsében egy utasítás áll, a self.name = name
. Itt egyszerűen a paraméterül kapott name
értékét eltároljuk a self.name
név alatt. Láthatólag a self.name
nem létezett korábban, de ez nem probléma: a self
-hez bármikor hozzáadhatunk egy új adattagot (property).
Ezek után a a meow()
már egy igen egyszerű metódus: ha meghívjuk akkor kiírjuk a konzolba hogy nyávog a macsek. Itt jól látható, hogy a self
-et kötelező volt kiírni, de azon kívül nincs más paramétere, azaz összességében nem vár semmilyen paramétert ez a metódus.
Nagyon-nagyon fontos megérteni, hogy a
self
az objektumra mutató paraméter (a self jelentése önmagam). Ebbe aself
-be a példányosításkor bekerülnek az osztályban definiált metódusok, amik példányosított entitásra lesznek érvényesek, de ezen kívül kvázi üres aself
objektum.
A példányosítás
Feljebb definiáltuk az osztályt. Ahogy (sokszor) írtam, ez csak egy tervrajz, az objektum működését írja le. Amikor elindul a program a Python értelmezi az osztályt de ettől még nem jön létre semmi objektum. Hasonlóan mint amikor a függvényt definiáltuk, az sem futott le addig amíg meg nem hívtuk meg.
A fenti példa utolsó két sorában készítünk egy élő példányt, egy objektumot az osztály alapján. Lássuk a kódot újra:
my_cat = Cat('Narancs')
print(my_cat.name)
my_cat.meow()
Az első sorban a Cat('Narancs')
kifejezéssel hozunk létre egy objektumot. Kinézetre ez pont olyan mint ha egy közönséges függvény hívnánk meg, de tudjuk hogy a Cat
most egy osztály.
Az osztály példányosítása során egy argumentumot adtunk meg, a 'Narancs'
stringet. A példányosításkor a konstruktor automatikusan (impliciten) meghívásra kerül, így a Cat
osztály __init__()
metódusába kerül a program futása, és ahogy korábban írtam, itt eltároljuk a name
paraméterben megkapott értéket ('Narancs'
) az objektumban önmagában, a self
segítségével.
Ezzel véget is ért a konstruktor futása, elkészült az objektum amit my_cat
névvel tároltunk el.
A fenti példa 2. sorában kiírjuk az objektum name
property-jének értékét. Ez ugye az adattag a konstruktor lefutásával létrejön, így minden gond nélkül megjelenik a macska neve a kimeneten.
Az utolsó sorban meghívjuk az objektum meow()
metódusát, és simán megjelenik a Meow
a konzolban.
Ha ugyan úgy kell egy osztály példányosítani mint egy függvényt meghívni, akkor honnan tudjuk hogy mikor-melyiket hívjuk meg? Mi van akkor, ha valaki nem az ajánlás szerint nevezi el az osztályát? A válasz egyszerű: nem tudjuk. De igazából az aztán tök mindegy hogy függvényt hívogatok-e ami visszatér egy objektummal, vagy egy osztályt példányosítottam, csak menjen amit akarok.
A metódusok definiálásának sorrendje
Azt már észrevehettük hogy egyáltalán nem mindegy a forráskódban, hogy az utasítások milyen sorrendben követik egymást. Például nem hivatkozhatunk egy változóra addig, amíg azt nem definiáltuk.
Az osztály esetében egy kicsit lazul ez a szabály: az osztályban a metódusok tetszőleges sorrendben kerülhetnek definiálásra. A metódusok hívásának a sorrendje lesz az, ami számít.
Macskák a dobozban
Mivel a macskák imádják a dobozokat, ezért gond nélkül belepakolhatjuk őket a Python kódban is. Egy tömbnyi macskát eltárolhatunk például egy list
-ben, így:
box = [Cat('Narancs'), Cat('Omlás'), Cat('Don Gatto'), Cat('Vörös Harry'), Cat('Félszemű Babylon')]
Ez után ha kiírjuk a tömb elemeit egymás alá:
for cat in box:
print(cat)
Akkor (elsőre meglepően) ezt az izét látjuk:
<__main__.Cat object at 0x7ff7daec1160>
<__main__.Cat object at 0x7ff7daec16d8>
<__main__.Cat object at 0x7ff7daec1710>
<__main__.Cat object at 0x7ff7daec1748>
<__main__.Cat object at 0x7ff7daec1780>
Ha közvetlenül kiírunk egy objektumot a print()
-el akkor, mivel senki se mondta meg a Pythonnak, hogy a Cat
kiírása alatt mint értünk, jobb híján az objektum osztályának a nevét (“teljes nevét”, modullal együtt) és a memória címét kapjuk eredményül, amivel itt most nem megyünk sokra.
Kitérő: az objektumok kiírása
Azt, hogy az objektum hogyan írható ki, a __repr__()
magic methodban tudjuk megadni. A print()
ezt a metódust automatikusan meghívja majd a kiírás pillanatában.
Ez egy elég rövid fejlesztés, de azért jobb ha látjuk az egész osztályt:
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print('Meow')
def __repr__(self):
return '<Cat object. Name: {0}>'.format(self.name)
Az utolsó két sorban látható az újdonság. A metódus neve fix, és paramétereket se kell neki adni (ez szabály). A metódus törzsében tetszőleges utasítások állhatnak, de kötelező a végén visszatérni egy str
-el. Esetünkben ez egy egyszerű kis format string.
Ezzel a kis módosítással ha újra futtatjuk a kódot akkor már ez jelenik meg a kimeneten:
<Cat object. Name: Narancs>
<Cat object. Name: Omlás>
<Cat object. Name: Don Gatto>
<Cat object. Name: Vörös Harry>
<Cat object. Name: Félszemű Babylon>
Azért ez elég fasza nem? A beépített magic methodok között rengeteg ilyen érdekes metódus van még, de erről külön bejegyzések fognak majd szólni.
Természetesen ahogy a
print()
meghívta a mi metódusunkat, úgy ezt mi is megtehetjük. Erre használható arepr()
függvény, de akár ilyet is írhatunk:print(cat.__repr__())
A második osztályunk Pythonban
Nem kétség, hogy a list
kiválóan őrzi a benne felsorolt objektumokat, mivel az egy általános tároló: bármit belerakhatunk, bármit kivehetünk belőle.
A valóságban nem biztos hogy jó ötlet lenne az amúgy is zsúfolt dobozba véletlenül egy blökit is belerakni. Szóval lehet érdemes lenne írni valami tároló objektumot, amibe csak a megadott típusú objektumok kerülhetnek bele, és a zsúfoltság elkerülése végett korlátozva van a mérete is. Ilyet egyik beépített Python tároló se tud, úgyhogy írjunk egy sajátot!
Tervezés
Nem kell aggódni, attól hogy közvetlenül a list
-et nem tudjuk használni a probléma megoldására, még nem kell újra feltalálnunk a tömböt. Egy olyan osztályt írunk, ami elfed magában egy list
-et. Ez azt jelenti, hogy az osztályunkban ugyan lesz egy tömb, de ahhoz csak mi saját metódusainkkal nyúlhatunk hozzá. Ide fogjuk elhelyezni az olyan ellenőrzéseket, mint hogy:
- Az osztályhoz hozzáadott objektum
Cat
típusú-e? - Nincs-e tele az objektumunk macskákkal?
Lássuk akkor szép sorban hogy mit-és-hogyan tudjon majd az osztály:
- Paraméterül lehessen megadni neki, hogy mekkora legyen a tároló mérete. Az alapértelmezett érték legyen mondjuk 3.
- A konstruktorban hozzunk létre egy üres tömböt
- Legyen az osztálynak egy
add_cat()
metódusa. Itt mindig ellenőrizzük, hogy a paraméterCat
típusú-e, és hogy nincs-e tele a tároló
- Ha minden rendben akkor hozzáadjuk a konstruktorban létrehozott tömbhöz a paraméterül kapott objektumot
- Ha valami gond van, akkor dobunk egy hibát
- Lehessen kiírni a tároló objektumunkat, hogy lássuk hogy mi van benne
Oké, egyetlen fontos kérdés maradt: mi lesz az osztály neve? CatBox
, természetesen.
Megvalósítás
class Cat:
def __init__(self, name):
self.name = name
def meow(self):
print('Meow')
def __repr__(self):
return '<Cat object. Name: {0}>'.format(self.name)
class CatBox:
def __init__(self, size=3):
self.size = size
self.cats = []
def add_cat(self, cat):
if isinstance(cat, Cat):
if len(self.cats) <= self.size:
self.cats.append(cat)
else:
raise MemoryError('The box is full (it has {0} cats}.'.format(self.size))
else:
raise TypeError('Expected a Cat instance, got {0} instead'.format(type(cat)))
def __repr__(self):
return repr(self.cats)
Csak úgy ömlik az új információ, mi?
A kód tetején a Cat
-ben semmi változás nincs, így ezzel nem is foglalkozunk.
A CatBox.__init__()
metódusnak egy paramétere van, a size
, azaz a macskás doboz mérete. A konstruktoron belül eltároljuk a kapott argumentumot azért, hogy később a macskák hozzáadásánál elvégezhessük vele szemben az ellenőrzést. Ugyan itt létrehozunk egy tömböt is cats
néven. Ide kerülnek majd be a macsekok. Ezzel meg is vagyunk.
Az add_cat()
metódus már combosabb, úgyhogy nézzük még egyszer:
def add_cat(self, cat):
if isinstance(cat, Cat):
if len(self.cats) <= self.size:
self.cats.append(cat)
else:
raise MemoryError('The box is full (it has {0} cats}.'.format(self.size))
else:
raise TypeError('Expected a Cat instance, got {0} instead'.format(type(cat)))
Egy paramétert vár, aminek cat
lesz a neve.
A 2. sorban az isinstance()
segítségével történik a típus ellenőrzése. Ez a függvény két paramétert vár: az első egy objektum a második pedig egy osztály (illetve általánosabbat: egy tetszőleges típus). Válaszul azt kapjuk meg, hogy az objektum osztálya az-e ami a második paraméterében szerepel.
Ez így elsőre furcsának tűnhet, de igen, magával az osztállyal is végezhetünk műveleteket (itt például egy feltételben használtuk), ugyan úgy, mint ahogy a függvényeknél is tehettük.
Ha ez a feltétel teljesül, akkor a paraméter biztosan Cat
típusú, szóval haladhatunk lefelé, viszont ha nem megfelelő osztályú az objektum, akkor egy TypeError
exceptiont dobunk el.
Egy pillanatra álljunk meg az exception szövegénél, mert van ott is egy új függvény:
'Expected a Cat instance, got {0} instead'.format(type(cat))
A type()
függvénnyel megkapjuk a cat
objektum típusát. Hangsúlyozom, az eredmény egy típus lesz, ami a cat
objektum esetén maga a Cat
osztály. Az osztály kiírásával az osztály nevét kapjuk egyébként (többé-kevésbé). Így lesz egy tök csinos hibaüzenetünk, amiben látszódik hogy milyen típusú objektumot vártunk, és mit kaptunk helyette.
Lejjebb, a 3. sorban ellenőrizzük, hogy nincs-e tele a tároló. Egyszerűen csak összehasonlítjuk, hogy a konstruktorban létrehozott tömb mérete kisebb vagy egyenlő-e a konstruktorban megadott limitnél. Ha itt is tovább tudunk haladni, akkor hozzáadjuk a self.cats
tömbhöz a paraméterül kapott objektumot, és készen is vagyunk.
Ha a self.cats
mérete túl nagy, akkor a másik ágban eldobunk egy MemoryError
exceptiont, ami paraméterül kap egy kis szöveget, csak hogy tudjuk hogy mi történt.
A
MemoryError
, mint aTypeError
is, a Python egy-egy beépített osztálya, ezért nincsenek korábban definiálva vagy importálva sehol sem.Azok után amit idáig tanultunk remélem feltűnt, hogy a
MemoryError
vagyTypeError
elnevezése és használata pont olyan, mint a közönséges osztályoké. Ez nem túl meglepő: ők tényleg egyszerű, sima osztályok.
Zárásként az osztály aljára odakerült a __repr__()
, amivel az objektum kiírása lehetséges lesz majd. Ezen belül az objektum saját cats
tömbjére meghívjuk a repr()
függvényt, és visszatérünk az így kapott értékkel. Ez a függvényhívás végigjárja a tömb összes objektumát, és meghívja azoknak a __repr__()
metódusait, amit kidolgoztunk a Cat
osztályban. Itt aztán jó mélyen belementünk a függvény hívogatásba, de a vége tök jó lesz, nyugi.
Ezzel nagyjából készen is vagyunk. Akkor példányosítsunk pár macskát és lássuk mi történik!
Próba
my_box = CatBox()
my_box.add_cat(Cat('Narancs'))
my_box.add_cat(Cat('Omlás'))
my_box.add_cat(Cat('Don Gatto'))
Itt bizony már nincs semmi újdonság. Három macska objektum került a my_box
-ba, úgyhogy tudjuk hogy tele a tároló. Írjuk ki az objektumot még mielőtt tovább haladnánk:
print(my_box)
Ennek kell megjelennie:
[<Cat object. Name: Narancs>, <Cat object. Name: Omlás>, <Cat object. Name: Don Gatto>]
Nem mondom, elég szexi.
Próbáljuk ki, hogy jól működnek-e az ellenőrzéseink. Rakjunk be egy negyedik macskát a dobozba:
my_box.add_cat(Cat('Vörös Harry'))
Meg is kapjuk a jól megérdemelt exceptiont:
MemoryError: The box is full (it has 3 cats).
Ennek az értelmezésébe nem megyünk bele, mert a hibakezelésről szóló bejegyzésemben már meséltem ezekről a hibaüzenetekről.
Ha gyorsan összedobunk egy Dog
osztályt, monduk így:
class Dog:
def __init__(self, name):
self.name = name
Igen, ez az osztály nem csinál semmi hasznosat se, de nem
Cat
, és most csak ez a lényeg.
És megpróbáljuk berakni a tárolóba:
my_box.add_cat(Dog('Scooby'))
Akkor ezt a szépséget kapjuk:
TypeError: Expected a Cat instance, got <class '__main__.Dog'> instead
Elég világos az üzenet szerintem.
Pihenő
Sikeresen elindultunk az OOP ösvény hosszú útján, de most már ideje egy kis pihenőt venni. Ezen a témán egy pár bejegyzés erejéig még csámcsogunk majd, mert igen sok elég fasza lehetőséget kínál ez a nyelvi elem, meg úgy az egész szemlélet. A következő bejegyzésben elővesszük a öröklődés fogalmát, meg a generikusságot is … talán.
Ez a bejegyzés a Python tutorialom egyik része. Az összes rész listája itt fellelhető.
-slp
Szia Slapec!
VálaszTörlésNincs kedved folytatni?
Üdv, Gábor
Szia! Jó a tutorial! Folytatod légyszi?
VálaszTörlésCsatlakozom az előttem irókhoz. Carry on..
VálaszTörlés