6. Objektorienteeritud programmeerimine
Siiamaani oleme salvestanud andmeid erinevatesse andmestruktuuridesse: järjendid, ennikud, hulgad, sõnastikud. Vaatamata on veel jäänud vaieldamata kõige võimsam andmestruktuur üldse. See on andmestruktuur, millega luuakse teisi andmestruktuure. See on andmestruktuur, mida ei saa võibolla isegi kutsuda andmestruktuuriks, sest see on omaette klass.
Jutt käib justnimelt klassidest ja nendega loodavatest objektidest. Tegu on objektorienteeritud programmeerimise peatükiga. Vaatame, kuidas oleme juba varem klassidega kokku puutunud, kuidas luua enda klasse, mis nendega teha saab ning kus neid hästi rakendada saab. Proovime luua enda versiooni mõnest populaarsest rakendusest.
Ettevalmistus
Selle peatüki läbimiseks piisab Pythoni installatsioonist: midagi paigaldama ei pea.
Et peatükist aru saada, peab olema läbitud õpiku esimene osa.
Tuttavate objektide uurimine
Kuigi siin kursusel pole veel klasside telgitagustesse vaadatud, oleme mõningaid klasse kasutanud. Tuletame meelde datetime
moodulit. See on moodul, mis lubab salvestada ajahetki, sh kuupäevi ja kellaaegu.
Mooduli importimine:
>>> import datetime
Moodulis datetime
asub klass nimega datetime. Klassidega saab teha isendeid ehk objekte. Need on muutujad, mille küljes võib olla teisi muutujaid ehk atribuute. Need muutujad võivad olla ka funktsioonid, mida kutsutakse meetoditeks. Et luua uut objekti, peab klassi välja kutsuma kui funktsiooni, mis tagastab selle klassi isendi. Proovime luua datetime
isendi. Funktsiooni parameetriteks lähevad aastaaeg, kuu, päev, tund, minut, sekund ja mikrosekund.
>>> kuupäev = datetime.datetime(2020, 2, 29, 12, 34, 56, 789)
Muutujasse kuupäev
on nüüd salvestatud klassi datetime isend ehk objekt. Uurime, mis on selle muutujatüüp (type()
) ning mis muutujad sellega seostuvad (dir()
).
>>> type(kuupäev) <class 'datetime.datetime'> >>> dir(kuupäev) ['__add__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__radd__', '__reduce__', '__reduce_ex__', '__repr__', '__rsub__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', 'astimezone', 'combine', 'ctime', 'date', 'day', 'dst', 'fold', 'fromisocalendar', 'fromisoformat', 'fromordinal', 'fromtimestamp', 'hour', 'isocalendar', 'isoformat', 'isoweekday', 'max', 'microsecond', 'min', 'minute', 'month', 'now', 'replace', 'resolution', 'second', 'strftime', 'strptime', 'time', 'timestamp', 'timetuple', 'timetz', 'today', 'toordinal', 'tzinfo', 'tzname', 'utcfromtimestamp', 'utcnow', 'utcoffset', 'utctimetuple', 'weekday', 'year']
Näeme, et tegemist on tõepoolest isendiga klassist datetime.datetime
, millel on palju seotud muutujaid ehk välju. Näiteks on seal väljad year
, month
, day
, hour
, minute
, second
ja microsecond
, millega saame kätte arvud, millega me isendi lõime. Neid välju nimetatakse isendiväljadeks.
>>> kuupäev.year 2020 >>> kuupäev.month 2 >>> kuupäev.microsecond 789
Klassidel saab tihti isendiväljade väärtusi ise määrata (nt kuupäev.year = 2021
), aga datetime seda ei luba.
Loodud isendil on ka funktsioone, mis kasutavad mõnda isendivälja. Neid nimetatakse isendimeetoditeks. Näiteks on üks selline strftime()
, mis tagastab kuupäeva sõnena soovitud formaadis.
>>> kuupäev.strftime <built-in method strftime of datetime.datetime object at 0x7f279e256570> >>> kuupäev.strftime("%Y-%m-%d %H:%M:%S") '2020-02-29 12:34:56'
Klassidel on tihti isendimeetodeid, mis muudavad isendivälju, aga datetime
klassil selliseid pole.
Klassidel on ka funktsioone, mis ei vaja isendeid ega isendivälju. Nendega saab üldiselt teha midagi selle klassiga seotut, näiteks sama klassi isendi tagastamist. Selliseid funktsioone kutsutakse staatilisteks meetoditeks. Sisuliselt on need lihtsalt funktsioonid, mis on klassi küljes. Klassil datetime
on näiteks staatiline meetod now()
, mis tagastab praeguse ajahetke jaoks datetime
isendi.
>>> datetime.datetime.now() datetime.datetime(2020, 2, 29, 18, 20, 33, 30651)
Selliseid meetodeid saab ka välja kutsuda isendite kaudu, aga üldiselt nii ei tehta.
>>> kuupäev.now() datetime.datetime(2020, 2, 29, 18, 20, 33, 30652) >>> datetime.datetime.now().now().now().now() datetime.datetime(2020, 2, 29, 18, 20, 33, 30653)
Klassidel võib ka olla staatilisi välju, mis on kõikidel isenditel samad. Klassil datetime
on näiteks staatilised väljad min
ja max
, mis näitavad minimaalseid ja maksimaalseid kuupäeva väärtuseid.
>>> datetime.datetime.min datetime.datetime(1, 1, 1, 0, 0) >>> datetime.datetime.max datetime.datetime(9999, 12, 31, 23, 59, 59, 999999)
Väljade ja meetodite selgituste saamiseks saab kasutada dokumentatsiooni ja help()
funktsiooni.
>>> help(datetime.datetime) Help on class datetime in module datetime: class datetime(date) | datetime(year, month, day[, hour[, minute[,second[,microsecond[,tzinfo]]]]]) | | The year, month and day arguments are required. tzinfo may be None, or an | instance of a tzinfo subclass. The remaining arguments may be ints. | | Method resolution order: | datetime | date | builtins.object | | Methods defined here: | | __add__(self, value, /) | Return self+value. | ...
Järgmisena vaatame, kuidas klasse luua ning kuidas need töötavad.
Klasside loomine
Loome enda kuupäeva klassi. Et vähendada tööd, mõtleme ainult kuupäevadele ilma kellaaegadeta ehk meie kuupäeva klassil peaks olema 3 isendivälja: aasta
, kuu
ja päev
. Meie ideaalses maailmas on kalendris igal kuul täpselt 31 päeva, muidu peaks kontrollima kuu päevade arvu vastavalt kuule.
Klassidele on tavaks panna suure algustähega nimi, seega paneme selle nimeks Kuupäev
. Klasse saab luua class
käsuga. Selle käsu alla saab defineerida meetodeid ja välju. Lisame alguses ainult aasta
välja ning määrame selle väärtuseks 2020
.
class Kuupäev: aasta = 2020
Nüüd saame luua selle klassi isendi. Selleks kutsume seda välja kui funktsiooni. Täpselt nagu tavaliste muutujatega, saame isendivälju kasutada ning üle kirjutada. Saame ka uusi välju omistada ning neid kasutada.
>>> kuupäev = Kuupäev() >>> kuupäev <__main__.Kuupäev object at 0x7fa1cf208e80> >>> kuupäev.aasta 2020 >>> kuupäev.aasta = 2021 >>> kuupäev.aasta 2021 >>> kuupäev.kuu = 2 >>> kuupäev.kuu 2
Konstruktorid
Praegu on igal uuel loodud isendil aasta
väärtus automaatselt 2020
. See ei ole väga ettenägelik lähenemine, sest sellest järgnevatel aastatel ei ole see enam kasulik ja iga uue isendi loomisel peab selle üle kirjutama. Samuti on tüütu isendi loomisel manuaalselt teisi muutujaid määrata.
Kui me lõime uue datetime isendi, kutsusime me klassi välja nagu funktsiooni ja lisasime muutujate väärtused parameetritena.
>>> kuupäev = datetime.datetime(2020, 2, 29, 12, 34, 56, 789)
Et sellist võimalust enda klassile lisada, peame defineerima erilise funktsiooni, mida kutsutakse konstruktoriks. Selle funktsiooni nimi Pythonis on __init__
ning selle esimene parameeter peab olema self
, mille väärtuseks läheb alati käesolev isend. Järgnevad parameetrid võivad olla ise määratud. Need on need parameetrid, mis lähevad funktsiooni sulgudesse selle väljakutsumisel.
class Kuupäev: def __init__(self, aasta, kuu, päev): self.aasta = aasta self.kuu = kuu self.päev = päev
Defineerisime klassile Kuupäev
konstruktori kolme parameetriga: aasta
, kuu
ja päev
. Kuna self
tähendab käesolevat isendit, siis määrame uuele isendile uued isendiväljad, mille väärtused võtame funktsiooni parameetritelt samamoodi, nagu me enne omistasime isendile välju.
Nüüd saame luua uue isendi ise valitud algväärtustega.
>>> kuupäev = Kuupäev() TypeError: __init__() missing 3 required positional arguments: 'aasta', 'kuu', and 'päev' >>> kuupäev = Kuupäev(2020, 2, 29) >>> kuupäev.aasta 2020 >>> kuupäev.kuu 2 >>> kuupäev.päev 29
Konstruktoris saame ka kontrollida väärtuseid. Kui kuu või päeva väärtus on sobivatest väärtustest väljaspool, viskame erindi. Meie klassil on kõikidel kuudel on teadagi 31 päeva.
class Kuupäev: def __init__(self, aasta, kuu, päev): self.aasta = aasta if 1 <= kuu <= 12: self.kuu = kuu else: raise ValueError("Kuu peab olema 1 kuni 12.") if 1 <= päev <= 31: self.päev = päev else: raise ValueError("Päev peab olema 1 kuni 31.")
>>> kuupäev = Kuupäev(2020, 13, 1) ValueError: Kuu peab olema 1 kuni 12. >>> kuupäev = Kuupäev(2020, 2, -3) ValueError: Päev peab olema 1 kuni 31. >>> kuupäev = Kuupäev(2020, 2, 29) >>>
Isendimeetodid
Kui me tahame praegu kuupäeva isendit teisendada sõneks, siis peame kokku liitma 3 isendivälja.
>>> kuupäev = Kuupäev(2020, 2, 29) >>> "{}-{:02d}-{:02d}".format(kuupäev.aasta, kuupäev.kuu, kuupäev.päev) '2020-02-29'
Võime lisada klassile isendimeetodi, mis seda ise teeb ning tagastab kuupäeva sõnevormingus. Asendame lihtsalt isendi viite muutujaga self
.
def sõnena(self): return "{}-{:02d}-{:02d}".format(self.aasta, self.kuu, self.päev)
>>> kuupäev = Kuupäev(2020, 2, 29) >>> kuupäev.sõnena() '2020-02-29'
Tegelikult peaks igal klassil olema selline meetod, mis väljendab isendit sõnena. Kui proovime isendit praegu sõneks teisendada, siis antakse meile klassi nimi ja mäluviide, aga isendi sisu ei näidata.
>>> str(kuupäev) '<__main__.Kuupäev object at 0x7feca1b2cc70>' >>> kuupäev <__main__.Kuupäev object at 0x7feca1b2cc70>
Kui aga datetime
klassi isendit sõneks teisendada, siis tuleb ülevaade isendiväljadest.
>>> kuupäev = datetime.datetime(2020, 2, 29, 12, 34, 56, 789) >>> str(kuupäev) '2020-02-29 12:34:56.000789' >>> kuupäev datetime.datetime(2020, 2, 29, 12, 34, 56, 789)
See, mida sõneks teisendamisel tehakse, oleneb jälle ühest erilisest meetodist nimega __str__
. Kui sõneks teisendatakse, võetakse selle meetodi tagastus. Kirjutame oma kuupäeva klassile selle meetodi. Võime kasutada sõnena meetodit selle tagastuses.
def __str__(self): return self.sõnena()
>>> kuupäev = Kuupäev(2020, 2, 29) >>> str(kuupäev) '2020-02-29'' >>> kuupäev.__str__() '2020-02-29' >>> kuupäev <__main__.Kuupäev object at 0x7f7f85ea5c70>
Sõneks teisendamine töötab hästi, aga niisama isendi kirjutamine tagastab jälle klassi nime ja mäluviite. See on sellepärast, et see väärtus võetakse teisest meetodist nimega __repr__
(tuleb sõnast representation). Saame enda klassile selle meetodi kirjutada. Tavaks on sellel meetodil tagastada sõne, mille abil saab konstruktorit välja kutsuda, et saada selline isend.
def __repr__(self): return "Kuupäev({}, {}, {})".format(self.aasta, self.kuu, self.päev)
>>> kuupäev = Kuupäev(2020, 2, 29) >>> kuupäev Kuupäev(2020, 2, 29) >>> repr(kuupäev) 'Kuupäev(2020, 2, 29)'
Lisame veel isendimeetodi, mis muudab isendiväljade väärtuseid. Näiteks võiks olla meetod aastate juurde lisamiseks.
def lisa_aastaid(self, aastate_arv): self.aasta += aastate_arv
>>> kuupäev = Kuupäev(2020, 2, 29) >>> kuupäev.lisa_aastaid(5) >>> kuupäev Kuupäev(2025, 2, 29)
Kuude lisamine on keerulisem, sest kui kuu väärtus on üle 12, siis peab vastavalt aastaid juurde lisama.
def lisa_kuid(self, kuude_arv): self.kuu += kuude_arv if self.kuu > 12: # Lisame aastaid juurde nii palju, kui neid 12-st üle läheb self.lisa_aastaid((self.kuu - 1) // 12) # Uus kuude arv on praeguse arvu jääk jagamisel 12-ga self.kuu = (self.kuu - 1) % 12 + 1
>>> kuupäev = Kuupäev(2020, 2, 29) >>> kuupäev.lisa_kuid(13) >>> kuupäev Kuupäev(2021, 3, 29) >>> kuupäev.lisa_kuid(120) >>> kuupäev Kuupäev(2031, 3, 29)
Proovi ise lisada päevade lisamise meetod. Kui kasutada lisa_kuid
meetodit, siis ei pea aastaid eraldi juurde lisama. Lihtsuse mõttes võib ikka eeldada, et igal kuul on 31 päeva.
Kui kõik kolm meetodit on tehtud, saab ka teha ühise liitmismeetodi.
def lisa(self, aastate_arv=0, kuude_arv=0, päevade_arv=0): self.lisa_aastaid(aastate_arv) self.lisa_kuid(kuude_arv) self.lisa_päevi(päevade_arv)
>>> kuupäev = Kuupäev(2020, 2, 29) >>> kuupäev.lisa(aastate_arv=1, kuude_arv=2, päevade_arv=3) >>> kuupäev Kuupäev(2021, 5, 1)
Staatilised meetodid
Lisame oma klassile ühe staatilise meetodi. Staatiliste meetodite puhul peab lihtsalt parameetri self
ära jätma, sest ei kasutata ühtegi isendivälja ega -meetodit. Kirjutame meetodi, mis tagastab isendi praeguse kuupäeva väärtustega. Võtame väärtused datetime
isendist.
def praegu(): nüüd = datetime.datetime.now() return Kuupäev(nüüd.year, nüüd.month, nüüd.day)
>>> Kuupäev.praegu() Kuupäev(2020, 2, 29)
Proovi kirjutada staatiline meetod parsi, mis võtab parameetrina sõne formaadis "YYYY-MM-DD", nt "2020-02-29" ning tagastab uue kuupäeva isendi nende väärtustega.
>>> Kuupäev.parsi("2020-02-29") Kuupäev(2020, 2, 29)
Kokkuvõte
Vaatasime natuke Pythoni objektorienteeritud programmeerimise võimalusi: mis asjad on klassid ja isendid, kuidas neid luua ja kasutada ning mis on, isendiväljad ja meetodid. Mõned võimalused jäid vaatamata, näiteks privaatsed väljad, klasside pärilus, itereeritavad isendid, objektide käitumine liitmisel, lahutamisel, võrdlemisel jne. Nendega saab tutvuda Pythoni dokumentatsioonis.
Programmeerimise paradigmat, mis kasutab objekte, nimetatakse objektorienteeritud programmeerimiseks. See on väga levinud viis, kuidas programme kirjutada ning sellest räägitakse rohkem aines "Objektorienteeritud programmeerimine" (LTAT.03.003), kus kasutatakse Java keelt. Kui on plaanis edaspidi Pythoniga tegeleda, siis on Pythoni objektorienteerituse tundmine väga kasulik.
Enesekontrolliküsimused
Ülesanded
1. Lõpeta kuupäeva klass. Peavad olema realiseeritud meetodid lisa_päevi
ja parsi
. Meetodid peavad arvestama päevade arvuga kuudes: jaanuaris on 31, veebruaris on 28-29, märtsis on 31, aprillis on 30 jne. Veebruari päevade arv sõltub aastast: kui aasta jagub arvuga 4, siis on 29, välja arvatud siis, kui see jagub arvuga 100, siis on 28, välja arvatud siis, kui see jagub arvuga 400, siis on jälle 29.Aastal 1900 oli veebruaris 28 päeva, aastal 2000 oli veebruaris 29 päeva.
2. Mõtle välja klass, mis võib sulle kasuks tulla ning kirjuta programm selle klassi defineerimise ja kasutusnäidetega. Klassil peab olema konstruktor, vähemalt üks isendiväli, vähemalt üks isendimeetod ning meetodid __str__
ja __repr__
.
Mõned ideed:
- Tabeli klass, kuhu saab lisada ridu ja veerge. Meetodid võiksid olla rea ja veeru kätte saamiseks indeksitega. Tabelit võiks saada teisendada CSV-vormingusse.
- Koordinaadi klass, millel on x- ja y-koordinaatide väärtuste isendiväljad. Meetod tagastab teise koordinaadi objekti kauguse.
- Kujundi klass (näiteks kolmnurk, ristkülik või ring), millel on külgede pikkused ja nurkade suurused. Meetodid pindala ja ümbermõõtude arvutamise jaoks. Võiks ka olla meetod, mis joonistab selle kujundi mooduliga
turtle
.