11. MODUL · PYTHON

Osztályok (OOP).

Osztály, objektum, metódus, öröklődés

1. fejezet

Osztály és objektum

Eddig az adatokat és a velük dolgozó függvényeket külön kezeltük: egy diák nevét, jegyeit külön változókban vagy egy szótárban, az átlagszámítást egy külön függvényben. Az objektumorientált programozás (OOP) összefogja őket: egy egységbe kerül az adat és az, amit vele csinálni lehet.

Osztály, objektum (példány)

Az osztály egy tervrajz: leírja, milyen adatai (attribútumai) és milyen műveletei (metódusai) lesznek az ilyen típusú dolgoknak. Az objektum (példány) egy konkrét dolog, amely a tervrajz alapján készült, saját adatokkal. Egy osztályból tetszőleges számú objektum készülhet.

Valójában eddig is objektumokkal dolgoztál: az "alma" a str osztály egy példánya, ezért van upper() metódusa; a [3, 1, 2] a list osztályé, ezért van append()-je. Most saját osztályt írunk.

szo = "alma"
lista = [3, 1, 2]
print(type(szo), szo.upper())
print(type(lista))
lista.append(4)
print(lista)

2. fejezet

Az első osztály: class, __init__, self

class Kutya:
    def __init__(self, nev, kor):
        self.nev = nev
        self.kor = kor

    def bemutatkozik(self):
        return f"{self.nev} vagyok, {self.kor} éves."


bodri = Kutya("Bodri", 3)
morzsa = Kutya("Morzsa", 7)
print(bodri.nev)
print(morzsa.bemutatkozik())
bodri.kor = bodri.kor + 1
print(bodri.bemutatkozik())
  • class Kutya:osztálydefiníció; a név PascalCase
  • __init__a konstruktor (pontosabban inicializáló): a példány létrehozásakor fut le, és beállítja az adatait
  • selfaz éppen kezelt objektum – mindig az első paraméter
  • self.nev = nevattribútum létrehozása a példányban
  • Kutya("Bodri", 3)példányosítás: új objektum

Kövesd Lépésenként. A Kutya("Bodri", 3) hívásakor a Python létrehoz egy üres objektumot, majd meghívja rá az __init__-et: a self ez az új objektum, a nev és a kor a kapott argumentumok. A self.nev = nev sor az objektumba „írja” az adatot. A Memóriában két külön Kutya objektum doboz jelenik meg, saját attribútumokkal, és a bodri meg a morzsa név nyíllal mutat rájuk.

A self-et a hívásnál nem adjuk át – a Python teszi meg helyettünk: a bodri.bemutatkozik() valójában Kutya.bemutatkozik(bodri). Ha a metódus fejlécéből lemarad a self, a hívás TypeError-t ad („takes 0 positional arguments but 1 was given”). Az __init__ két oldalán két-két aláhúzás áll.

3. fejezet

Metódusok és az objektum állapota

Az objektum attribútumainak pillanatnyi értéke az objektum állapota. A metódusok olvassák és módosítják ezt az állapotot – és vigyáznak rá, hogy érvényes maradjon. Egy bankszámla egyenlege például ne lehessen negatív, és negatív összeget se lehessen befizetni:

class BankSzamla:
    def __init__(self, tulajdonos, egyenleg=0):
        self.tulajdonos = tulajdonos
        self.egyenleg = egyenleg

    def befizet(self, osszeg):
        if osszeg <= 0:
            return False
        self.egyenleg += osszeg
        return True

    def kivesz(self, osszeg):
        if osszeg <= 0 or osszeg > self.egyenleg:
            return False
        self.egyenleg -= osszeg
        return True

    def atutal(self, cel, osszeg):
        if self.kivesz(osszeg):
            cel.befizet(osszeg)
            return True
        return False


anna = BankSzamla("Anna", 10000)
bela = BankSzamla("Béla")
print(anna.kivesz(25000))
print(anna.atutal(bela, 3000))
print(anna.befizet(-500))
print(anna.tulajdonos, anna.egyenleg, "|", bela.tulajdonos, bela.egyenleg)

Az atutal metódus egy másik számlaobjektumot kap paraméterként, és annak a metódusát hívja. A lépésenkénti nézetben a self és a cel két különböző objektumra mutat.

Mit ír ki?

class Szamlalo:
    def __init__(self):
        self.ertek = 0

    def novel(self):
        self.ertek += 1

a = Szamlalo()
b = a
c = Szamlalo()
a.novel()
b.novel()
c.novel()
print(a.ertek, b.ertek, c.ertek)
  1. 2 2 1
  2. 1 1 1
  3. 2 1 1

A b = a nem hoz létre új objektumot: az a és a b ugyanarra az egy számlálóra mutat (mint a listáknál a 4. modulban). Ezt kétszer növeltük. A c egy második, független objektum.

4. fejezet

Szöveges megjelenítés és összehasonlítás: speciális metódusok

Ha egy saját objektumot kiírunk, alapból valami ilyesmit kapunk: <__main__.Konyv object at 0x7f…>. A speciális metódusokkal (dupla aláhúzásos, angolul „dunder” nevekkel) megmondhatjuk, hogyan viselkedjen az objektumunk a Python beépített műveleteiben:

MetódusMikor hívódik meg?
__str__(self)print(obj), str(obj), f-string – felhasználóbarát szöveg
__repr__(self)repr(obj), illetve ha az objektum listában vagy szótárban jelenik meg – programozói leírás
__eq__(self, masik)a == b
__lt__(self, masik)a < b – és ezzel a sorted(), min(), max() is működik
__add__(self, masik)a + b
__len__(self)len(obj)
class Konyv:
    def __init__(self, cim, szerzo, oldal):
        self.cim = cim
        self.szerzo = szerzo
        self.oldal = oldal

    def __str__(self):
        return f"{self.szerzo}: {self.cim} ({self.oldal} oldal)"

    def __repr__(self):
        return f"Konyv({self.cim!r}, {self.szerzo!r}, {self.oldal})"

    def __lt__(self, masik):
        return self.oldal < masik.oldal


polc = [
    Konyv("Egri csillagok", "Gárdonyi Géza", 560),
    Konyv("Pál utcai fiúk", "Molnár Ferenc", 240),
    Konyv("Légy jó mindhalálig", "Móricz Zsigmond", 330),
]
print(polc[0])
print(polc)
for k in sorted(polc):
    print(k)
print("A legrövidebb:", min(polc))

A !r az f-stringben a repr() alakot kéri – így a szövegek idézőjelben jelennek meg, ahogy a kódban írnánk őket.

Pontok összeadása: __add__ és __eq__

class Vektor:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, masik):
        return Vektor(self.x + masik.x, self.y + masik.y)

    def __eq__(self, masik):
        return self.x == masik.x and self.y == masik.y

    def __str__(self):
        return f"({self.x}, {self.y})"


a = Vektor(1, 2)
b = Vektor(3, 4)
c = a + b
print(c)
print(c == Vektor(4, 6), a == b)

Az a + b a háttérben a.__add__(b) hívás, és egy új Vektor objektumot ad vissza – az a és a b nem változik. __eq__ nélkül a == azt vizsgálná, hogy ugyanaz az objektum-e a kettő, nem azt, hogy egyenlő-e a tartalmuk. (Jó tudni: ha egy osztály megadja az __eq__-t, a példányai alapból nem tehetők halmazba és nem lehetnek szótárkulcsok – ehhez egy __hash__ metódus is kellene.)

5. fejezet

Osztályváltozó és példányváltozó

A self.valami attribútumok a példányhoz tartoznak: minden objektumnak saját van belőlük. Az osztály törzsében (a metódusokon kívül) létrehozott változó viszont az osztályhoz tartozik, és minden példány közösen látja. Jó például állandókhoz és az összes példányra vonatkozó adatokhoz (hány objektum készült).

class Termek:
    AFA = 0.27
    darab = 0

    def __init__(self, nev, netto):
        self.nev = nev
        self.netto = netto
        Termek.darab += 1

    def brutto(self):
        return round(self.netto * (1 + Termek.AFA))


t1 = Termek("füzet", 400)
t2 = Termek("toll", 250)
print(t1.brutto(), t2.brutto())
print("Létrehozott termékek:", Termek.darab)
print(t1.AFA)

Csapda: ha egy osztályváltozónak a self-en keresztül adsz értéket (self.darab += 1), nem a közös változó nő, hanem a példányban jön létre egy új, azonos nevű attribútum, amely eltakarja az osztályét. Az osztályváltozót ezért az osztály nevén keresztül módosítsd: Termek.darab += 1.

Mit ír ki?

class Gomb:
    kattintas = 0

    def katt(self):
        self.kattintas += 1

a = Gomb()
b = Gomb()
a.katt()
a.katt()
b.katt()
print(a.kattintas, b.kattintas, Gomb.kattintas)
  1. 3 3 3
  2. 2 1 3
  3. 2 1 0

A self.kattintas += 1 a példányba ír: az első híváskor kiolvassa az osztályváltozót (0), és a példányban létrehoz egy saját kattintas attribútumot (1). Így az a-nak 2, a b-nek 1 saját kattintása lesz, az osztályváltozó pedig 0 marad. Ha a közös számlálót akarjuk növelni: Gomb.kattintas += 1.

6. fejezet

Ellenőrzött attribútumok: @property

Mi akadályozza meg, hogy valaki egy téglalap oldalát negatívra állítsa (t.a = -5)? A @property dekorátorral egy attribútum olvasását és írását metódusok mögé rejthetjük: kívülről továbbra is t.a-nak látszik, de az értékadáskor lefut az ellenőrzés. Így számított attribútumot is készíthetünk (pl. a terület), amelyet mindig az aktuális oldalakból számolunk.

class Teglalap:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    @property
    def a(self):
        return self._a

    @a.setter
    def a(self, ertek):
        if ertek <= 0:
            raise ValueError("Az oldal hossza pozitív szám legyen!")
        self._a = ertek

    @property
    def b(self):
        return self._b

    @b.setter
    def b(self, ertek):
        if ertek <= 0:
            raise ValueError("Az oldal hossza pozitív szám legyen!")
        self._b = ertek

    @property
    def terulet(self):
        return self.a * self.b


t = Teglalap(3, 4)
print(t.terulet)
t.a = 10
print(t.terulet)
t.b = 0

Figyeld meg: a konstruktor is a self.a = a formát használja, így már a létrehozáskor lefut az ellenőrzés (ha közvetlenül a „belső” _a-ba írna, megkerülné azt). A _a előtti aláhúzás megállapodás: „belső használatra, kívülről ne nyúlj hozzá”. A hibaüzenet pedig azt mondja, ami a szabály: pozitív legyen – a nulla sem megengedett.

Kivétel kiváltása: raise

A raise ValueError("…") szándékosan kivételt (hibát) vált ki a megadott üzenettel. A program ott megáll – hacsak a hívó egy try–except blokkban (10. modul) el nem kapja. Így jelzi az osztály a hívónak, hogy érvénytelen értéket kapott, ahelyett hogy csendben hibás állapotba kerülne.

Dekorátor: a függvény fölötti @valami sor módosítja, hogyan viselkedjen az alatta álló függvény. A @property attribútummá „alakítja” a metódust. Két további gyakori dekorátor: a @staticmethod olyan függvényt jelöl, amely logikailag az osztályhoz tartozik, de nem használ self-et; a @classmethod pedig az osztályt kapja első paraméterként (cls), és tipikusan alternatív létrehozó módokhoz használjuk (pl. Homerseklet.fahrenheitbol(98.6)).

7. fejezet

Öröklődés

Az öröklődéssel egy meglévő osztályból (szülő, ősosztály) újat készítünk (gyerek, leszármazott). A gyerek mindent megkap, amit a szülő tud, ehhez hozzáadhat új attribútumokat és metódusokat, és felülírhatja (override) a szülő metódusait. A szülő metódusát a super()-rel érjük el – leggyakrabban a konstruktorban.

class Allat:
    def __init__(self, nev):
        self.nev = nev

    def hang(self):
        return "…"

    def bemutatkozik(self):
        return f"{self.nev} vagyok, és ezt mondom: {self.hang()}"


class Kutya(Allat):
    def hang(self):
        return "Vau!"


class Madar(Allat):
    def __init__(self, nev, szarnyfesztav):
        super().__init__(nev)
        self.szarnyfesztav = szarnyfesztav

    def hang(self):
        return "Csip-csirip!"

    def repul(self):
        return f"{self.nev} repül ({self.szarnyfesztav} cm-es szárnyakkal)."


allatok = [Kutya("Bodri"), Madar("Csőrike", 25), Allat("Valaki")]
for a in allatok:
    print(a.bemutatkozik())
print(allatok[1].repul())
print(isinstance(allatok[0], Kutya), isinstance(allatok[0], Allat))

Figyeld meg: a bemutatkozik() metódust csak az Allat-ban írtuk meg, mégis minden állat a saját hangját mondja, mert a self.hang() mindig az objektum tényleges osztályának metódusát hívja. Ezt nevezzük többalakúságnak (polimorfizmus): ugyanaz a hívás a különböző típusú objektumoknál különbözőképpen viselkedik. A Madar konstruktora a super().__init__(nev) hívással a szülőre bízza a név beállítását, és csak a saját új attribútumáról gondoskodik.

Egy osztálynak több szülője is lehet (többszörös öröklődés, pl. class DronRobot(Repulo, Robot):). A Python támogatja, de bonyolulttá teszi a programot; kezdő szinten ritkán van rá szükség.

8. fejezet

Gyakorló feladatok

A gép az osztályodat példányosítja és a metódusait hívja – az osztály, a metódusok és az attribútumok neve egyezzen a feladatban megadottal.

Diák osztály

Írj Diak osztályt, amelynek konstruktora a nevet (nev) és az osztályt (osztaly) kapja, és mindkettőt attribútumként tárolja. Legyen egy bemutat() metódusa, amely a tárolt névből és osztályból összeállított szöveget adja vissza, vesszővel elválasztva. Például a Diak("Kiss Anna", "10.B") objektumnál: Kiss Anna, 10.B.

class Diak:
    pass

d = Diak("Kiss Anna", "10.B")
print(d.bemutat())
class Diak:
    def __init__(self, nev, osztaly):
        self.nev = nev
        self.osztaly = osztaly

    def bemutat(self):
        return f"{self.nev}, {self.osztaly}"


d = Diak("Kiss Anna", "10.B")
print(d.bemutat())
Tipp

Konstruktor: def __init__(self, nev, osztaly):, benne self.nev = nev. A metódus első paramétere is self, és return-nel adja vissza a szöveget.

Jegyek nyilvántartása

Írj Tanulo osztályt: a konstruktor a nevet kapja (nev), és egy üres jegyek listát hoz létre. Metódusok: uj_jegy(jegy) felveszi a jegyet, ha 1 és 5 közé esik (feltételezheted, hogy egész számot kap), és True-t ad vissza, különben nem veszi fel, és False-t ad; atlag() a jegyek átlagát adja (üres listánál None-t). Minden tanulónak saját jegylistája legyen.

class Tanulo:
    def __init__(self, nev):
        self.nev = nev
        self.jegyek = []

    def uj_jegy(self, jegy):
        if 1 <= jegy <= 5:
            self.jegyek.append(jegy)
            return True
        return False

    def atlag(self):
        if not self.jegyek:
            return None
        return sum(self.jegyek) / len(self.jegyek)


anna = Tanulo("Anna")
anna.uj_jegy(5)
anna.uj_jegy(4)
print(anna.nev, anna.jegyek, anna.atlag())
Tipp

A listát az __init__-ben hozd létre: self.jegyek = []. Ha az osztály törzsében hoznád létre (osztályváltozóként), minden tanuló ugyanazt a listát használná.

Hőmérséklet két skálán

Írj Homerseklet osztályt, amely belül Celsius-fokban tárol (konstruktor: celsius=0). Legyen celsius és fahrenheit property-je, mindkettő olvasható és írható (F = C · 9/5 + 32). Az abszolút nulla (−273,15 °C) alatti értéket a celsius setter ValueError-ral utasítsa el.

h = Homerseklet(100)
h.fahrenheit        →  212.0
h.fahrenheit = 32
h.celsius           →  0.0
class Homerseklet:
    def __init__(self, celsius=0):
        self.celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, ertek):
        if ertek < -273.15:
            raise ValueError("Az abszolút nulla alatti hőmérséklet nem létezik.")
        self._celsius = ertek

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

    @fahrenheit.setter
    def fahrenheit(self, ertek):
        self.celsius = (ertek - 32) * 5 / 9


h = Homerseklet(100)
print(h.fahrenheit)
h.fahrenheit = 32
print(h.celsius)
Tipp

A fahrenheit setter ne a _celsius-ba írjon közvetlenül, hanem a self.celsius = … formával – így az ellenőrzés itt is lefut. A konstruktor is ezt a formát használja.

Rendezhető könyvek

Írj Konyv osztályt (cim, szerzo, oldal). A str() adja ezt: Molnár Ferenc: Pál utcai fiúk (240 oldal). Két könyv legyen egyenlő, ha a címük és a szerzőjük azonos, és a < az oldalszám szerint hasonlítson (így a sorted() oldalszám szerint rendez).

class Konyv:
    def __init__(self, cim, szerzo, oldal):
        self.cim = cim
        self.szerzo = szerzo
        self.oldal = oldal

    def __str__(self):
        return f"{self.szerzo}: {self.cim} ({self.oldal} oldal)"

    def __eq__(self, masik):
        return self.cim == masik.cim and self.szerzo == masik.szerzo

    def __lt__(self, masik):
        return self.oldal < masik.oldal


k = Konyv("Pál utcai fiúk", "Molnár Ferenc", 240)
print(k)
Tipp

Három speciális metódus kell: __str__, __eq__ és __lt__. Mindegyik a self és a masik attribútumait veti össze.

Számlatípusok örökléssel

Adott a Szamla ősosztály. Készíts belőle két leszármazottat, amelyek a kivesz(osszeg) metódust felülírják:

  • MegtakaritasiSzamla(egyenleg=0, minimum=1000): csak akkor enged kivenni, ha utána legalább minimum marad a számlán;
  • HitelSzamla(egyenleg=0, hitelkeret=100000): az egyenleg -hitelkeret-ig mehet le.

Mindkettő konstruktora a super().__init__(egyenleg) hívással állítsa be az egyenleget. A kivesz sikeres levételnél True-t, egyébként False-t adjon (és ne változtasson az egyenlegen); nem pozitív összeget soha ne fogadjon el.

class Szamla:
    def __init__(self, egyenleg=0):
        self.egyenleg = egyenleg

    def befizet(self, osszeg):
        if osszeg > 0:
            self.egyenleg += osszeg

    def kivesz(self, osszeg):
        if 0 < osszeg <= self.egyenleg:
            self.egyenleg -= osszeg
            return True
        return False


# Ide jön a két leszármazott osztály
class Szamla:
    def __init__(self, egyenleg=0):
        self.egyenleg = egyenleg

    def befizet(self, osszeg):
        if osszeg > 0:
            self.egyenleg += osszeg

    def kivesz(self, osszeg):
        if 0 < osszeg <= self.egyenleg:
            self.egyenleg -= osszeg
            return True
        return False


class MegtakaritasiSzamla(Szamla):
    def __init__(self, egyenleg=0, minimum=1000):
        super().__init__(egyenleg)
        self.minimum = minimum

    def kivesz(self, osszeg):
        if osszeg > 0 and self.egyenleg - osszeg >= self.minimum:
            self.egyenleg -= osszeg
            return True
        return False


class HitelSzamla(Szamla):
    def __init__(self, egyenleg=0, hitelkeret=100000):
        super().__init__(egyenleg)
        self.hitelkeret = hitelkeret

    def kivesz(self, osszeg):
        if osszeg > 0 and self.egyenleg - osszeg >= -self.hitelkeret:
            self.egyenleg -= osszeg
            return True
        return False


m = MegtakaritasiSzamla(5000)
h = HitelSzamla(0, 20000)
print(m.kivesz(4500), m.kivesz(3000), m.egyenleg)
print(h.kivesz(15000), h.kivesz(10000), h.egyenleg)
Tipp

class MegtakaritasiSzamla(Szamla): – a zárójelben a szülő. A konstruktorban előbb super().__init__(egyenleg), aztán a saját attribútum. A kivesz feltételében a „kivét utáni” egyenleget vizsgáld: self.egyenleg - osszeg.

9. fejezet

Összefoglalás

FogalomLényeg
class Nev:osztály = tervrajz; Nev(…) új objektumot (példányt) hoz létre
__init__(self, …)konstruktor: beállítja az új objektum attribútumait
selfaz aktuális objektum; a metódusok első paramétere, hívásnál nem adjuk át
példány- / osztályváltozóself.x objektumonként; az osztály törzsében: közös
__str__, __eq__, __lt__, __add__kiírás, ==, < (rendezés), +
@propertyellenőrzött vagy számított attribútum
class Gyerek(Szulo):, super()öröklődés, felülírás, a szülő metódusának hívása

Gyakorold tovább: modellezz egy egyszerű könyvtárat: Konyv (cím, szerző, kölcsönözve-e) és Konyvtar (könyvek listája; felvesz, kolcsonoz(cim), visszahoz(cim), elerheto() metódusok). Gondold végig: melyik adat melyik osztályhoz tartozik, és mit adjon vissza egy-egy metódus, ha a könyv nem található?