Loeng 12: Objektorienteeritud programmeerimine (OOP), klass, konstruktor ja spetsiaalne meetod __init__, objekt, meetodi defineerimine, klassi staatiline muutuja, spetsiaalne meetod, pärilikkus, plümorfism, lausend: class, muutuja self, funktsioonid: isinstance, issubclass, jm.¶
Koodinäited¶
1 Objektorjenteeritud programmeerimine (OOP)¶
1.1 Miks on OOP kasulik?¶
Osa teist on enda teadmata juba programmeerinud kasutades OOPi. Pythonis on kõik andmetüübi esindajad OOP objektid. Pythoni andmetüübid on defineertud klassides. Me oleme selle ideega, et andmetüüp = klass, juba põgusalt kohtunud. Meie jaoks on andmetüüpi nimi olnud klassi nimi. Teatavasti saab funktsiooniga type järgi vaadata objekti andmetüüpi:
print(type([]))
print(type (1))
print(type(True))
print(type(1.2))
print(type(None))
print(type(print))
print(type(type))
<class 'list'> <class 'int'> <class 'bool'> <class 'float'> <class 'NoneType'> <class 'builtin_function_or_method'> <class 'type'>
Siiamaani oleme probleeme lahendades kasutanud Pythonit skriptimiskeelena ja tuginedes funktsionaalprogrameerimise võtetele. Milleks on meile vajalik OOP?
- Nagu just mainitud on Python ülesse ehitatud objektorjeteerituse baasil. Seega selleks, et teada kuidas keelt paremini ja oskuslikumalt kasutada võiksime OOPi teada.
- Meile teadaolevad võtted pole piisavalt võimekad ega mugavad isegi suhteliselt lihtsate probleemide lahendamiseks.
1.1.1 Näide suhteliselt lihtsast probeelmist mida saaks lahendada kasutades OOPi¶
TalTechi tudengite andmetetega töötlemine. Tudengite andmete näide on näidatud Tabelis 1.
| Tudeng | Tudeng 1 | Tudeng 2 | ... | Tudeng n |
|---|---|---|---|---|
| Nimi | Juulius Tipikas | Juulia Tipikas | ... | Mari Karu |
| Sünniaeg | 1. sept 1918 | 2. sept 1922 | ... | 2. sept 2022 |
| Isikukood | 982674752 | 182374658 | ... | 8347542934 |
| Telefon | 112 | 6207777 | ... | 4207667 |
| Hinded | - | 4, 5, 5 | ... | 5, 5, 5 |
Teatavasti on Taltechi tudengite arv n tuhandetes.
Esitame probleemi püstituse ja selle kaks lahendust kasutades meile teadaolevaid programmeerimise võtteid.
1.1.2 Probleem ja lahendus 1¶
Rumal lahendus:
# Loome kõigi tudengite andmeid sisaldavad listid.
tudeng1 = ['Juulius Tipikas', '1. sept 1918', 982674752, 112, []]
tudeng2 = ['Juulia Tipikas', '2. sept 1922', 182374658, 6207777, [4, 5, 5]]
# Püüame andmeid organiseerida.
Tudengid = [tudeng1, tudeng2]
# Nime järgivaatamine. Olen huvitatud Juuliusest.
print(Tudengid[0][0])
# Lisame Juuliusele hinde.
Tudengid[0][4].append(5)
# Hinnete järgivaatamine.
print(Tudengid[0][4])
Juulius Tipikas [5]
Siia on suhteliselt lihtne andmeid, näiteks konkreetse tudengi uut hinnet, sisestada. Mis saab aga siis kui Andmekaitse Inspektsioon leiab, et sina ei tohi tudengite telefoninumbreid säärasel kujul salvestada ja sa pean kõik telefoninumbrid kustutama:
# Kuidagi peab kõigi tudengite listid läbi käima.
del tudeng1[-2]
del tudeng2[-2]
print(Tudengid[0]) # Telefoninumber kustutatud.
print(Tudengid[1]) # Telefoninumber kustutatud.
['Juulius Tipikas', '1. sept 1918', 982674752, [5]] ['Juulia Tipikas', '2. sept 1922', 182374658, [4, 5, 5]]
1.1.3 Lahendus 2¶
Natukene targem lahendus, kuid siiski suhteliselt ebamugav.
# Koondame tudengite andmed sõnaraamtusse.
tudeng1 = {
'nimi': 'Juulius Tipikas',
'sünnipäev': '1. sept 1918',
'isikukood': 982674752,
'telefon': 112,
'hinded': []
}
tudeng2 = {
'nimi': 'Juulia Tipikas',
'sünnipäev': '2. sept 1922',
'isikukood': 182374658,
'telefon': 6207777,
'hinded': [4, 5, 5]
}
# Püüame andmeid organiseerida.
Tudengid = {
'tudeng1': tudeng1,
'tudeng2': tudeng2
}
# Nime järgivaatamine. Olen huvitatud Juuliusest.
print(Tudengid['tudeng1']['nimi'])
# Lisame Juuliusele hinde.
Tudengid['tudeng1']['hinded'].append(5)
# Hinnete järgivaatamine.
print(Tudengid['tudeng1']['hinded'])
Juulius Tipikas [5]
Siia on suhteliselt lihtne andmeid, näiteks konkreetse tudengi uut hinnet, sisestada. Mis saab aga siis kui Andmekaitse Inspektsioon leiab, et sina ei tohi tudengite telefoninumbreid säärasel kujul salvestada ja sa pean kõik telefoninumbrid kustutama:
# Kuidagi peab kõigi tudengite sõnaraamatud läbi käima.
del tudeng1['telefon']
del tudeng2['telefon']
print(Tudengid['tudeng1']) # Telefoninumber kustutatud.
print(Tudengid['tudeng2']) # Telefoninumber kustutatud.
{'nimi': 'Juulius Tipikas', 'sünnipäev': '1. sept 1918', 'isikukood': 982674752, 'hinded': [5]}
{'nimi': 'Juulia Tipikas', 'sünnipäev': '2. sept 1922', 'isikukood': 182374658, 'hinded': [4, 5, 5]}
Eelmainitud näidet saab lahendada paremini kasutades OOPi. OOP organiseerib andmed automaatselt, lihtsustab andmete sisestamist ja haldamist. OOP lubab üksikasjalikult defineerida andmete struktuuri (nt. vt. Tabel 1 esimene veerg) mida saab omakorda kasutada konkreetsete objektide ja nendega seotud andmete haldamiseks (nt. vt. Tabel 1 esimeses reas olevad tudengid).
1.2 OOP ja Python¶
| Joonis 1. Programmeerimine Pythonis. Programm ja selle osad. |
Väljavõte Loengust 1 vt. Slaidid 1:
Python on väga kõrge tasemega üldotstarbeline dünaamiline objektorienteeritud skriptimiskeel. Python võimaldavad kasutada erinevaid programmeerimise paradigmasid nagu funktsionaalseid, protseduraalseid, ja objektorienteeritud programmeerimise võtteid, vrd. Joonis 1.
- Funktsionaalne programmeerimine: funktsiooni rakendamisel argumendile (sisendile) saadakse väärtus. Funktsioonid on taaskasutatavad.
- Protseduraalne programmeerimine: programmi poolt tehtavad arvutused ja tegevused on koondatud nn. protseduuri blokkidesse, mida saab taaskasutada.
- Objektorienteeritud programmeerimine (OOP): programmi töös manipuleeritakse objektidega, mis on spetsiaalsed andmestruktuurid objekti andmetest (argumentidest, muutujatest) ja objektiga seotud tegevustest (meetodid). Objekti võib käsitleda kui iseseisvat üksust, mis oskab andmeid vastu võtta, neid töödelda, salvestada ja väljastada.
1.3 OOP programmeerimise paradigma¶
OOPis opereerime objektidega ja klassidega lisaks muutujatele, avaldistele, erinevatele koodi konstruktsioonidele, funktsioonidele ja protseduuridele. OOPis käsiteme objekte kui iseseisvaid üksusi mis oskavad andmeid vastu võtta, neid töödelda, salvestada ja väljastada. Objektid defineeritakse ja luuakse Pythoni plokkis nimega klass (defineerime lausendiga class).
Klass (class) -- objekti loomise eeskiri millega käivad kaasas klassi ja selle objekte kirjeldavad atribuudid (andmed, muutujad) ja objektiga seotud tegevused (meetodid, spetsiaalsed/maagilised meetodid, jne.). Eeskiri milla alusel luuakse objekte.
1.3.1 OOP programmeerija maailmapilt 1¶
- Maailm koosneb:
- Füüsilistest ja mõttelistest asjadest (mõtle: objekt)
- Näiteks: autod, inimesed, mõisted
- Tegevustest asjadega
- Näiteks: auto käivitamine
- Asjade vahelistest suhetest
- Näiteks: liikluseeskirjad reguleerivad autode omavahelist liiklemist, füüsikaseadused määravad auto ja tee vahelise kontakti
- Füüsilistest ja mõttelistest asjadest (mõtle: objekt)
- Asju saab liigitada:
- Autod on sõidukid
- Veoauto ja sõiduauto on auto
- Jalgratas ei ole auto aga on sõiduk
- Inimesed ja koerad on elusolendid
- Asjadel on omadused:
- Auto värvus, võimsus, rataste arv, asukoht, kiirus
- Asjad koosnevad teistest asjadest:
- Auto koosneb mootorist, ratastest, roolist, jne.
- Liigid moodustavad pärinevuse puu:
- Päritakse omadusi ja tegevusi
- Sõiduauto $\rightarrow$ auto $\rightarrow$ mootorsõiduk $\rightarrow$ ... $\rightarrow$ füüsiline objekt $\rightarrow$ ... $\rightarrow$ element $\rightarrow$ molekul $\rightarrow$ aatom $\rightarrow$ elementaarosakesed
- Tegevused muudavad asjade omadusi:
- Auto kiiruse muutmine
- Asjad suhtlevad või vahetavad informatsiooni omavahel:
- Inimene juhib autot
- Asjade omadused ja tegevused on määratud asjade liigiga
- Asjad on enda liigi esindajad
1.3.2 OOP programmeerija maailmapilt 2¶
- Maailm programmeerimise terminites:
- Objekt esindab asja (object, class instance)
- Klass esindab asja liiki
- Funktsioon esindab üldist tegevust asjadega
- Meetod esindab liigiga seotud tegevust asjadega
- Atribuut/muutuja esindab asja omadust
- Liides (interface) määrab asjade vahelise suhtluse viisi (ei käsitle selles kursuses)
- Programm on maailma mudel:
- Töötav programm simuleerib maailmas toimuvat
- Töötav programm võib muuta maailma
- Maailm võib muuta programmi tööd
- Programmeerimine on maailma mudeli kirjeldamine
2 Klassi defineerimine ja objekti loomine¶
2.1 Klass kus puuduvad meetodid¶
Klassi defineerimiseks kasutame lausendit class. Klassi sisu eristame muust koodist taandega nii nagu ikka Pythoni plokkides. Klassi nimesid on soovitatav kirjutda suure algustähega nt. Foo. Nimes esinevaid sõnu eristame omakorda suure algustähega ja kirjutades klassinime kokku, nt. FooBar mitte Foo_Bar, vmt. Koodistiili ja dokumenteerimise kohat loe lisaks PEP 8 ja PEP 257.
Lihtne klass mis sisaldab nn. staatilisi klassimuutujaid. Klassimuutujaite skoop on globaalne. Muutujatele viitame kasutades klassi või selle objekti nime:
class A:
a = 1 # Klassi staatilised muutujad või atribuudid.
b = 2
print(A.a) # Klassimuutuja väljakutse, sulge pole.
print(A.b)
print(A)
1 2 <class '__main__.A'>
Objekti loomine kasutades eelmises koodirakus defineeritud klassi A. Üks klass lubab luua meelevaldse arvu objekte. Objekte nimetatakse ka klassi isendiks või instantsiks.
obj = A() # Objekti loomine, class instance. NB! sulud.
print(obj.a) # Klassimuutuja väljakutse kasutades objekti. Sulge pole.
print(obj.b)
print(obj)
print(type(obj)) # Mis andmetüüp? Klass = andmetüüp.
1 2 <__main__.A object at 0x104d9f950> <class '__main__.A'>
Real 1 oleva objekti defineerimiseks vajalik kood on programmeerija eest varjatud ja teostub automaatselt või maagiliselt. Python teostas automaatselt kõik vajaliku objekti loomiseks.
2.2 Klass koos konstruktoriga¶
Objektidega seotud andmete, nende struktuuri, organisatsiooni ning algse oleku ehk initsialiseeringu saame valida ise. Iga klassi abil loodud objektiga käivad kaasas selle enda andmed. Initsialiseeringu defineerimiseks kasutame klassiploki sees spetsiaalset/maagilist meetodit ehk funktsiooni nimega __init__. Spetsiaalset meetodit __init__ nimetatakse ka maagiliseks meetodiks. Spetsiaalne meetod __init__ omab kohustuslikku positsionaalset argumenti, millele pannakse tavaliselt nimeks self. Argumendiga self tähistame objekti mida saame luua kasutades hetkel kirjutatavat klassi, vt. Joonis 2 ja järgmist lõiku.
2.2.1 Näide: klass Auto¶
Auto ja selle mudel. See klass nimega Auto loob alati BMW-sid:
class Auto:
def __init__(self): # Konstruktor.
self.mudel = 'BMW' # Objekti muutuja/atribuut, instance variable.
auto = Auto() # Loon objekti "auto" kasutades klassi Auto.
print(auto.mudel)
BMW
Tavaliselt loome objekte dünaamilisemalt lisades konstruktorile argumente:
class Auto:
def __init__(self, mudel): # Konstruktor, toetab argumentide kasutust.
self.mudel = mudel # Objekti muutuja/atribuut, instance variable.
auto = Auto('BMW') # Loon objekti "auto" kasutades klassi Auto.
print(auto.mudel)
BMW
Joonis 2. Kostruktori __init__ esimene positsionaalne argument, millele pannakse tavaliselt nimeks self, ja selle tähendus. |
2.3 Klass koos enda kirjutatud meetoditega¶
Selleks, et meetodid siduda kindla objekti initsialiseeringuga kasutame ka nendes spetsiaalset argumenti või muutujat self (esimene positsionaalne argument). Joonis 3 näitab kuidas mõelda spetsiaalsest muutujast self tavaliste meetodite kontekstis.
2.3.1 Näide: klass Auto¶
Lisame klassile Auto kaks enda kirjutatud meetodit millega saame kontrollida auto kiirust ja asukohta:
class Auto:
def __init__(self, kiirus = 0): # Konstruktor, toetab kõiki argumenditüüpe.
self.kiirus = kiirus # Objekti muutuja/atribuut, instance variable.
def set_kiirus(self, kiirus): # Meetod 1.
self.kiirus = kiirus # Kirjutan väärtuse üle.
def get_asukoht(self, aeg): # Meetod 2
asukoht = self.kiirus * aeg
print('Auto asub', asukoht, 'km kaugusel.')
auto = Auto() # Loon objekti "auto" kasutades klassi Auto.
auto.set_kiirus(60) # Kiirus 0 on igav.
auto.get_asukoht(0.5)
Auto asub 30.0 km kaugusel.
Märkus: Muutuja ja argument self seob def-plokkide lokaalseid skoope. Mäletatavasti on Pythonis def-plokk ainuke lokaliseeriv skoop. Kui meetodi esimene argument on self siis sellel meetodil on juurdepääs konstruktori def-plokki muutujatele ehk objekti dünaamilistele muutujatele, vrd. Joonis 3.
Joonis 3. Kostruktori __init__ esimene positsionaalne argument, millele pannakse tavaliselt nimeks self, ja selle tähendus ning kasutus meetodites. |
Loon teise objekti nimega teine_auto kasutades sama klassi Auto.
teine_auto = Auto()
teine_auto.set_kiirus(100)
teine_auto.get_asukoht(2.0)
Auto asub 200.0 km kaugusel.
Lisame klassi Auto definitsioonile ka staatilise klassimuutuja:
class Auto:
värv = 'kollane' # Klassimuutuja, static variable.
def __init__(self, kiirus = 0): # Konstruktor.
self.kiirus = kiirus # Objekti muutuja.
def set_kiirus(self, kiirus):
self.kiirus = kiirus
def get_asukoht(self, aeg):
asukoht = self.kiirus * aeg
print('Auto asub', asukoht, 'km kaugusel.')
auto = Auto() # Loon objekti auto kasutades klassi Auto.
print(auto.värv) # Läbi objekti.
print(Auto.värv) # Läbi klassi.
auto.set_kiirus(60)
auto.get_asukoht(0.5)
kollane kollane Auto asub 30.0 km kaugusel.
3 Pärilikkus (inheritance), baas- või superklass ja alamklass¶
Pärimine on võte mis võimaldab kirjutatud koodi taaskasutamist.
3.1 Staatiliste klassimuutujate pärimine enda loodud superklassist¶
Klassi defineerimisel saame taaskasutada juba olemasolevate klasside (kogu) sisu. Sellist Pythoni klasside omadust nimetame pärilikkuseks. Sellist koodi taaskasutust nimetame pärimiseks.
Klassi mis pärandab enda sisu nimetame baas- ehk superklassiks ja klassi mis pärib sisu nimetame alamklassiks. Pärilikkus võimaldab koodi kirjutada kiiremini ja lisada objektidele funktsionaalsust ilma olemasolevate klasside definitsioonide muutmist.
Pärida saab mitmest klassist korraga (järgmise nädala loeng). Klassid saavad pärida teiste klasside staatilisi muutujaid, initsialiseeringuid (järgmise nädala loeng) ja meetodeid.
Lihtne näide kus pärime klassimuutujaid:
class A: # Baas- ehk superklass.
a = 1 # Klassi staatilised muutujad.
b = 2
c = 3
class B(A): # Alamklass B pärib superklassist A.
c = 33 # Klassimuutuja, kõik muu päritakse ülalt.
obj = B()
print(obj.a, obj.b, obj.c)
print(dir(obj)) # Sisaldab päritud andmeid a, b.
1 2 33 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'c']
3.2 Meetodite pärimine enda loodud superklassist¶
Sarnaselt statilistele muutujatele saame pärida meetodeid:
class Tudeng: # Baas- ehk superklass.
def tereta(self):
print("Tere!")
class Keemik(Tudeng): # Alamklass Keemik pärib superklassist Tudeng.
def aita_tudengit(self): # Klassi Keemik meetod.
print("See lektor on kohutav.")
juuli = Tudeng() # Loome klassi Tudeng objekti.
mati = Keemik() # Loome klassi Keemik objekti.
mati.tereta() # Meetod tereta päriti.
mati.aita_tudengit() # Klassi Keemik meetod.
juuli.tereta() # Klassi Tudeng meetod.
juuli.aita_tudengit() # Baasklassis Tudeng meetod puudub.
Tere! See lektor on kohutav. Tere!
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[14], line 20 17 mati.aita_tudengit() # Klassi Keemik meetod. 19 juuli.tereta() # Klassi Tudeng meetod. ---> 20 juuli.aita_tudengit() AttributeError: 'Tudeng' object has no attribute 'aita_tudengit'
3.3 Päritud meetodi ülekirjutamine (overriding) ja polümorfism¶
Kui sa soovid võid peale pärimist mõne meetodi üle kirjutada kasutades selle algset nime. Kehtima jääb ülekirjutatud meetod:
class Tudeng: # Baas- ehk superklass.
def tereta(self): # <---- Algne meetodi definitsioon.
print("Tere!")
def aita_tudengit(self):
print("See lektor on kohutav.")
class Keemik(Tudeng): # Pärime superklassist Tudeng.
def tereta(self): # <---- Kirjutan meetodi üle.
print("Tere! Keemikud on kõige targemad.")
juuli = Tudeng() # Loome klassi Tudeng objekti.
mati = Keemik() # Loome klassi Keemik objekti.
mati.tereta() # Klassis Keemik ülekirjutatud meetod.
mati.aita_tudengit() # Meetod aita_tudengit päriti.
juuli.tereta() # Klassi Tudeng meetod.
juuli.aita_tudengit() # Klassi Tudeng meetod.
Tere! Keemikud on kõige targemad. See lektor on kohutav. Tere! See lektor on kohutav.
Eelolevas näites sõltub meetodi tereta käitumine tudengi andmetüübist (Tudeng, Keemik) ehk objekti klassist.
Polümorfism (vt. ka Lõik 5) $-$ omadus kus sama (nimega) meetodi käitumine sõltub objekti klassist ehk objekti andmetüübist.
- Polymorphism $-$ etymology: word comes from the greek roots "poly" (many) and "morphe" (form). Unlike "form" in English, "morphe" does not mean "shape." It is a philosophical term that means "the outward expression of an inner essence."
3.4 Pythoni standardteegi klassid ja pärilikkus¶
Eespool nägime, et konstruktor __init__ pole klassi defineerimisel kohustuslik. Kuidas Python teab initsialiseerida objekte kui me ei kirjuta konstruktorit? Vastus: See meetod päritakse taustal vaikimisi ehk automaatselt koos muu vajaliku koodiga. Standardteegis eksisteerib baasklass nimega object (jah see on väikese tähega) mida kasutatakse selleks otstarbeks.
Taustal pärivad kõik klassid baasklassist nimege object. See tähendab, et klassi saab defineerida ka nii:
class Auto(object): # Pärimine superklassist object (NB! pole kohustulik).
värv = 'kollane'
def __init__(self, kiirus = 0):
self.kiirus = kiirus
def get_asukoht(self, aeg):
asukoht = self.kiirus * aeg
print('Auto asub', asukoht, 'km kaugusel.')
auto = Auto(60)
auto.get_asukoht(0.5)
Auto asub 30.0 km kaugusel.
Python võimaldab luua baasklassiga defineeritud objekti kasudades sisseehitatud funktsiooni object. Selliselt loodud objektile pole võimalik lisada uusi atribuute ega meetodeid.
obj = object()
print(obj)
print(dir(obj))
<object object at 0x1123daa50> ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
Info ja dokumentatsioon baasklassi object kohta:
import builtins
#help(object)
Pärime klassi list ja muudame selle meetodi append käitumist lisades sellele logi kirjutamise funktsionaalsuse:
class LoggedList(list):
def append(self, item): # Kirjutan üle.
print(f"Appending {item}")
list.append(self, item)
#super().append(item) # Nii saab ka, funktsioonist super räägime järgmine nädal.
ll = LoggedList() # Tühja listi loomine.
ll.append(10)
ll.append(20)
print(ll)
Appending 10 Appending 20 [10, 20]
3.5 Method resolution order (MRO)¶
Päritud klasside, meetodite ja muutujate prioriteetsus määratakse MRO eeskirjaga. MRO eeskiri dikteerib mis järjekorras omistatakse päritud ja ülekirjutatud atribuutidele ning meetoditele väärtusi (mis klassi skoobist otsime väärtusi). MRO eeskiri on sarnane LEGB reegliga mida tutvustasime Loengus 6. MRO info on eriti kasulik kui uuritav klass pärib mitmest erinevast superklassist.
class A:
a = 1
b = 2
class B(A):
b = 3
obj = B()
print(obj.a, obj.b) # Muutuja b väärtus kirjutati üle.
1 3
MRO eeskirja saab igal ajal järgi vaadata, nt. lugedes klassi vaikimisi genereeritud dokumentatsiooni:
help(B) # Info sisaldub klassi abimatejalis ja dokumentatsioonis.
Help on class B in module __main__: class B(A) | Method resolution order: | B | A | builtins.object | | Data and other attributes defined here: | | b = 3 | | ---------------------------------------------------------------------- | Data descriptors inherited from A: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined) | | ---------------------------------------------------------------------- | Data and other attributes inherited from A: | | a = 1
Altenatiivselt ja lühema väljundiga infot tagastab meetod mro:
B.mro()
[__main__.B, __main__.A, object]
või kasutades muutujat __mro__:
B.__mro__
(__main__.B, __main__.A, object)
4 Spetsiaalsed ehk maagilised meetodid (special, magic or dunder methods)¶
Nimetus "maagiline" viitab asjaolule, et sellised meetodid teostavad automaatselt teatud kasulikke tegevusi. Tegevused teostatakse lihtsalt sellepärast, et programeerija kasutab nende nimesid.
4.1 Valik spetsiaalseid ehk maagilisi meetodeid¶
Maagilised meetodid on tavaliselt kirjutataud kasutades selle nime ees ja taga kahte alakriipsu __nimi__. Nimi "dunder method" tuleb väljendist "double underscore" $\rightarrow$ "double" + "under" $\rightarrow$ "dunder".
Valik maagilisi meetodeid:
__init__- Konstruktor funktsioon.__str__- Objekti esitused stringina (inimloetavam). Meetodile vastab funktsioonstr.__repr__- Objekti esitused stringina. Meetodile vastab funktsioonrepr.__lt__,__eq__,__gt__, jm. $-$ Võrdlusoperaatorid ja tehted mis päritakse superklassistobjectobjektide võrdlemiseks (allpool laiendame teemat). Nendele meetoditele vastavad Pythoni operaatorid<,==ja>, jm.__add__,__sub__,__mul__, jm. $-$ Aritmeetilised tehted objektidega (allpool laiendame teemat). Nendele meetoditele vastavad Pythoni operaatorid+,-ja*, jm.
class Auto:
def __init__(self, kiirus, mudel, hind): # Konstruktor on maagiline meetod.
self.kiirus = kiirus # Objekti muutuja.
self.mudel = mudel
self.hind = hind
auto = Auto(12, 'BMW', 4000) # Loon objekti.
# Need maagilised meetodid päriti klassist object.
print(auto.__str__())
print(auto.__repr__())
print(auto)
print(str(auto)) # Maagilisele meetodile vastav funktsioon str.
print(repr(auto)) # Maagilisele meetodile vastav funktsioon repr.
print(dir(auto)) # Lisaks __str__ ja __repr__ meetoditele eksisteerivad siin meie loodud objektimuutujaid hind, kiirus ja mudel.
<__main__.Auto object at 0x136fdd850> <__main__.Auto object at 0x136fdd850> <__main__.Auto object at 0x136fdd850> <__main__.Auto object at 0x136fdd850> <__main__.Auto object at 0x136fdd850> ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'hind', 'kiirus', 'mudel']
4.1.2 Spetsiaalse ehk maagilise meetodi ülekirjutamine (overriding, overloading)¶
Nii nagu tavalisi enda kirjutatud meetodeid, mida arutasime eespoole, saame ka spetsiaalseid vaikimisi kaasaantud meetodeid muuta:
- Neid ülekirjutades (overriding) $-$ meetodi sisu täielik muutus.
- Neid osaliselt ülekirjutades (overloading) $-$ meetodi funktsionaalsuse laiendamine.
Ülekirjutamine annab meile võimaluse meetodi käitumist muuta endaloodud objektidele sobivamaks. Selleks, et päritud meetodit üle kirjutada kasuta selle nime:
class Auto:
def __init__(self, kiirus, mudel, hind): # Konstruktor on maagiline meetod.
self.kiirus = kiirus # Objekti muutujad.
self.mudel = mudel
self.hind = hind
def __str__(self): # Kirjutan olemasoleva üle, kasutades selle nime.
return '{} auto hinnaga {} sõidab {} km/h.'.format(
self.mudel,
self.hind,
self.kiirus)
def __repr__(self): # Kirjutan olemasoleva üle, kasutades selle nime.
return "{}('{}', '{}', '{}')".format(
type(self).__name__,
self.mudel,
self.hind,
self.kiirus)
auto = Auto(12, 'BMW', 4000) # Loon objekti
print(auto)
print(str(auto)) # Sama mis eelmine.
print(repr(auto))
BMW auto hinnaga 4000 sõidab 12 km/h.
BMW auto hinnaga 4000 sõidab 12 km/h.
Auto('BMW', '4000', '12')
4.1.3 Võrdlus- ja aritmeetikaoperaatoritega seotud spetsiaalsed meetodid¶
Võrdlus- ja aritmeetikaoperaatorid on salvestatud baasklassis object, me nägime seda juba eespool Lõigus 3.4:
obj = object()
print(dir(obj))
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
4.1.3.1 Võrdlusoperaatorid¶
Võrdlusoperaatoreid käsitlesime Loengus 3. Võrdlus operaatorid https://docs.python.org/3/reference/datamodel.html#basic-customization:
object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)
Avaldise x < y taustal kutsutakse välja x.__lt__(y) või object.__lt__(x, y), jne.
x, y = 2, 5
x < y
True
x.__lt__(y)
True
int.__lt__(x, y) # Klass int on klassi object alamklass.
True
x, y = 12.4, 1.8
float.__lt__(x, y)
False
float.__gt__(x, y)
True
4.1.3.2 Aritmeetikaoperaatorid¶
Aritmeetikaoperaatoreid käsitlesime Loengus 3. Aritmeetika operaatorid https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types:
object.__add__(self, other)
object.__sub__(self, other)
object.__mul__(self, other)
object.__matmul__(self, other)
object.__truediv__(self, other)
object.__floordiv__(self, other)
object.__mod__(self, other)
object.__divmod__(self, other)
object.__pow__(self, other[, modulo])
object.__lshift__(self, other)
object.__rshift__(self, other)
Avaldise x + y taustal kutsutakse välja x.__add__(y) või object.__add__(x, y), jne.
x, y = 12.4, 1.8
x + y
14.200000000000001
x.__add__(y)
14.200000000000001
float.__add__(x, y) # Klass float on klassi object alamklass.
14.200000000000001
4.1.4 Võrdlus- ja aritmeetikaoperaatorite ülekirjutamine¶
Sarnaselt vaikimisi kaasaantud spetsiaalsetele meetoditele saame ka Pythoni operaatoritele vastavaid spetsiaalseid meetodeid üle kirjutada. Näiteks saame luua enda defineeritud objektide liitmise operaatori ehk kirjutada operaatori + üle. Kusjuures me ei pea operaatoritega seotud klassi object ilmutatud kujul pärima, kuna mäletatavasti teostatakse pärimine taustal automaatselt.
Algse definitsiooni saad järgi vaadata Pythoni lähtekoodist (Spyder IDE: Ctrl + G), dokumentatsioonist või Internetist.
Pythoni aritmeetika- ja muid operaatorid saab enda kirjutatavas klassis alati üle kirjutada (operator overriding, operator overloading).
4.1.4.1 Liitmise operaatori ülekirjutamine¶
Liitmise operaator + ehk meetod __add__. Lisame liitmisele kommentaari:
class MyInt:
def __init__(self, väärtus):
self.väärtus = väärtus
def __add__(self, other): # Argument other on my_int objekt.
print('Vastus on: ', end='') # Meie panus :)
return self.väärtus + other.väärtus # Eeldan, et otheril muutuja olemas.
a1 = MyInt(1) # Täisarvu objekti loomine.
a2 = MyInt(3)
print(a1.__add__(a2))
print(MyInt.__add__(a1, a2))
print(a1 + a2) # Meetodile __add__ vastav operaator.
print(MyInt(2) + MyInt(4))
Vastus on: 4 Vastus on: 4 Vastus on: 4 Vastus on: 6
Natukene põhjalikum kood mis tagab parema operaatoti + töö:
class MyInt:
def __init__(self, väärtus):
self.väärtus = väärtus
def __add__(self, other): # Argument other on my_int objekt.
summa = self.väärtus + other.väärtus # Eeldan, et other'il muutuja olemas.
return MyInt(summa) # Loome uue objekti. Väljastab objekti andmetüübiga MyInt.
#return type(self)(summa) # Nii saab ka.
def __str__(self): # Esitus sõnena konsoolis ja mujal.
return f'{self.väärtus}'
def __repr__(self): # Meetodile vastab funtsioon repr.
return f'MyInt arv väärtudega {self.väärtus}.'
a1 = MyInt(1) # Täisarvu objekti loomine.
a2 = MyInt(2)
a3 = MyInt(3)
print(a1.__add__(a2).__add__(a3))
print(MyInt.__add__(MyInt.__add__(a1, a2), a3))
print(a1 + a2 + a3) # Meetodile __add__ vastav operaator.
print(repr(a1))
print(a1)
a1
6 6 6 MyInt arv väärtudega 1. 1
MyInt arv väärtudega 1.
4.1.4.2 Võrdlusoperaatori ülekirjutamine¶
Võrdlusoperaator == ehk meetod __eq__. Tehniliselt ja vaikimisi operaator == kontrollib kas objekti või muutja nimed viitavad samale objektile arvuti mälus (id(obejkt1) = id(objekt2)). Selline käitumine pole igas olukorras sobiv. Kirjutame operaatori üle nii, et see suudaks võrdseks lugeda objekte millede väärtused ehk sisu on samad olenemata objektide asukohast mälus (vt. lisaks Loeng 7 Lõik 3.2):
- Vaikimisi käitumine.
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
p1 = Point2D(1, 3) # Loon kaks punkti.
p2 = Point2D(1, 3) # Uus objekt sama sisu.
p3 = p2 # Samad objektid.
print(p1)
print(p2)
print(p3)
print(id(p1))
print(id(p2))
print(id(p3))
print(p1 == p2) # Erinevad objektid.
print(p3 == p2) # Nimed viitavad samale objektile.
<__main__.Point2D object at 0x136fde8d0> <__main__.Point2D object at 0x136fdf690> <__main__.Point2D object at 0x136fdf690> 5217577168 5217580688 5217580688 False True
- Ülekirjutatud käitumine. Kirjutama üle meetodid
__eq__ja ka__repr__:
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return '({}, {})'.format(self.x, self.y)
def __eq__(self, other): # Muudame vaikimisi käitumise.
if isinstance(other, Point2D):
return self.x == other.x and self.y == other.y
return False
p1 = Point2D(1, 3) # Loon kaks punkti.
p2 = Point2D(1, 3) # Uus objekt sama sisu.
p3 = p2 # Samad objektid.
print(p1)
print(p2)
print(p3)
print(id(p1))
print(id(p2))
print(id(p3))
print(p1 == p2) # Erinevad objektid.
print(p3 == p2) # Nimed viitavad samale objektile.
(1, 3) (1, 3) (1, 3) 5217667664 5217571344 5217571344 True True
Peale meetodi __eq__ ülekirjutamist on Pythonis endiselt võimalik kontrollida kas objektid on samad objekt arvuti mälus. Kasuta operaatorit is:
p1 is p2 # See võimalus jääb alati, kasutaja ei saa seda operaatorit muuta.
False
5 Pythoni sisseehitatud operaatorite ja funktsioonide polümorfism¶
NB! Tehniliselt polümorfismi definitsioon eeldab, et kuskil või kuidagi toimub nii pärimine (nt. klassist object) kui ka meetodi ülekirjutamine mis toimub alamklassis.
5.1 Polümorfne operaator¶
Näide: Liitmise operaator + on polümorfne.
x = y = 2
print(x + y)
print(x.__add__(y))
print(int.__add__(x, y))
4 4 4
x, y = 'a', 'b'
print(x + y)
print(x.__add__(y))
print(str.__add__(x, y))
ab ab ab
x, y = [1, 2], [3]
print(x + y)
print(x.__add__(y))
print(list.__add__(x, y))
[1, 2, 3] [1, 2, 3] [1, 2, 3]
5.2 Polümorfne funktsioon¶
Näide: Funktsioon len on polümorfne.
print(len("Programiz")) # Sõne tähemärgid.
print(str.__len__("Programiz")) # Vastav maagiline meetod.
9 9
print(len(["Python", "Java", "C"])) # Listi liikmed.
print(list.__len__(["Python", "Java", "C"])) # Vastav maagiline meetod.
3 3
print(len({"Name": "John", "Address": "Nepal"})) # Sõnaraamatu võtmeted.
print(dict.__len__({"Name": "John", "Address": "Nepal"})) # Vastav maagiline meetod.
2 2
6 Muud klassiga seotud teemad¶
6.1 Kasulikud sisseehitatud funktsioonid¶
Allolevad funktsiooni täiendavad Loengus 8 arutatud oluliste sisseehitatud funktsioone nimistut.
6.1.1 Funktsioonid: isinstance ja issubclass¶
Funktsioon isinstance kontrollib kas objekt (class instance) on loodud mingis konkreetses klassis? Kas objekt on kindla klassi instants? Kasuta süntaksit:
isinstance(<objekt>, <klass>)
isinstance(auto, Auto)
True
isinstance(auto, A)
False
Funktsioon issubclass kontrollib kas klass on teise klassi alamklass? Kasuta süntaksit:
issubclass(<alamklass>, <superklass>)
issubclass(B, A)
True
issubclass(A, B)
False
6.1.2 Funktsioonid: hasattr, getattr, setattr, delattr ja vars¶
Funktsioonid on seotud klassi- ja objektimuutujatega ehk atribuutidega.
Kasutamise süntaks (tutvu iseseisvalt):
hasattr(<object>, <name>)
getattr(<object>, <name>[, <default>])
setattr(<object>, <name>, <value>)
delattr(<object>, <name>)
vars([<object>])
Näide: Funktsioon setattr kasutus.
class Auto:
color = ''
def __init__(self, kiirus = 0):
self.kiirus = kiirus
auto = Auto()
setattr(Auto, 'color', 'green')
print(Auto.color)
setattr(auto, 'kiirus', 100) # Kui muutujat kiirust poleks siis luuaks.
print(auto.kiirus)
setattr(auto, 'vanus', 30) # Uus objektiga seotud muutuja. Luuakse.
print(auto.vanus)
green 100 30
Näide: Funktsioon getattr kasutus.
color = getattr(Auto, 'color')
auto_kiirus = getattr(auto, 'kiirus')
auto_vasus = getattr(auto, 'vanus')
print(color)
print(auto_kiirus)
print(auto_vasus)
green 100 30
class Tudeng:
"""A one-line docstring."""
def tereta(self):
"""A one-line docstring."""
print("Tere!")
class Tudeng:
"""A multi-line
docstring.
"""
def tereta(self):
"""A multi-line
docstring.
"""
print("Tere!")
6.3 Klasside importimisest¶
Siin kehtivad samad reeglid mida arutasime Loengutes 7 ja 8.
Kindla klassi importimine moodulist:
from <moodul> import <klass>
Kogu mooduli, mis sisaldab mitmeid klasse, importimine:
import <moodul>
callable?
Signature: callable(obj, /) Docstring: Return whether the object is callable (i.e., some kind of function). Note that classes are callable, as are instances of classes with a __call__() method. Type: builtin_function_or_method
Kuidas meetodit kasutada. Objekt muutub funktsioonilaadseks kui sellel on defineeritud meetod __call__.
Kui meetod on defineeritud siis saame objekti a välja kutsuda kahel erineval viisil:
a.__call__(arg1, arg2, ...)või lihtsamalt,a(arg1, arg2, ...).
6.4.1 Meetodi __call__ kasutus¶
Lihtne näide, programm mis väljastab sõne "Hello, Juulius!":
class Greeter:
def __call__(self):
print("Hello, Juulius!")
gf = Greeter() # Objekti loomine.
gf.__call__()
gf() # Objekt on väljakutsutav nagu funktsioon.
Hello, Juulius! Hello, Juulius!
Sama programm koos argumentidega:
class Greeter:
def __call__(self, name):
return f"Hello, {name}!"
gf = Greeter() # Objekti loomine.
print(gf.__call__("Juulius"))
print(gf("Juulius")) # Objekt on väljakutsutav nagu funktsioon.
Hello, Juulius! Hello, Juulius!
6.4.2 Meetod mis liidab, korrutab arvulisi väärtusi¶
Liitmise näide:
class Adder:
def __init__(self):
self.total = 0
def __call__(self, x):
self.total += x
return self.total
af = Adder()
print(af(5)) # 0 + 5 = 5
print(af(3)) # 5 + 3 = 8
print(af(2)) # 8 + 2 = 10
5 8 10
Korrutamise näide:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 2 * 5 = 10
print(triple(5)) # 3 * 5 = 15
10 15
6.4.3 Lihtne funktsioonikääre¶
Lihtne funktsioonikääre ehk funktsiooni ümbris:
class FuncWrapper:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Calling function...")
return self.func(*args, **kwargs)
def square(x):
return x * x
wrapped_square = FuncWrapper(square) # Objekti loomine.
print(wrapped_square(4))
Calling function... 16
6.5 Meetodi dekoreerimine enda kirjutatud dekoraatoriga¶
Sarnaselt sellele kuidas me dekoreerisime funktsioone saame dekoreerida klassi meetodeid. Dekoreerimiseks saame kasutada operaatorit @ ja süntaksit kujul <meetod> = <dekoraator>(<meetod>) Dekoraator mis lisab kommentaare ja klass ilme meetodita __init__:
def my_decorator(func):
def wrapper(self, *args):
print(f"Calling method {func.__name__}")
result = func(self, *args)
print(f"Method {func.__name__} finished")
return result
return wrapper
class MyClass:
@my_decorator
def say_hello(self, name):
print(f"Hello, {name}!")
obj = MyClass()
obj.say_hello("World!")
Calling method say_hello Hello, World!! Method say_hello finished
Klass milles on kasutatud konstruktorit __init__ ja kus dekoreeritakse kasutades operaatorit @:
class MyClass:
def __init__(self, name):
self.name = name
@my_decorator
def say_hello(self):
print(f"Hello, {self.name}!")
obj = MyClass('Juulius')
obj.say_hello()
Calling method say_hello Hello, Juulius! Method say_hello finished
Sama näide kus kasutame dekoreerimiseks süntaksit <meetod> = <dekoraator>(<meetod>):
class MyClass:
def __init__(self, name):
self.name = name
def say_hello(self):
print(f"Hello, {self.name}!")
say_hello = my_decorator(say_hello) # Dekoreerimine.
obj = MyClass('Juulius')
obj.say_hello()
Calling method say_hello Hello, Juulius! Method say_hello finished