1.1.1 Klass Auto¶
Kirjutame klassi mille abil saame luua auto objekte. Maagilise meetodi __init__ esimene positionaalne argumendiga self tähistame loodavat objekti ning selle kaudu defineeritakse objekti muutujaid ja muid seotud andmeid.
Märkus: Igal loodud objektiga käivad kaasas selle enda andmed ja meetodid.
class Auto:
def __init__(self, kiirus = 0): # Konstruktor.
self.kiirus = kiirus # Objektimuutuja.
def __repr__(self): # Maagiline meetod, esitus sõnena.
return 'Auto kiirusega {}.'.format(self.kiirus)
def set_kiirus(self, kiirus): # Meetod 1. Setter funktsioon.
self.kiirus = kiirus # Kirjutan olemasoleva väärtuse üle.
def get_asukoht(self, aeg): # Meetod 2. Getter funktsioon.
return self.kiirus * aeg
auto = Auto() # Loon auto objekti kasutades klassi Auto.
print(auto)
auto.set_kiirus(60) # [km/h]
print(auto.get_asukoht(0.5)) # [h]
print(repr(auto))
print(str(auto))
print(auto) # kasutab taustal str väljundit.
auto
Auto kiirusega 0. 30.0 Auto kiirusega 60. Auto kiirusega 60. Auto kiirusega 60.
Auto kiirusega 60.
Klassi abil saime luua mitu objekti. Loon teise auto objekti kasutades klassi Auto:
teine_auto = Auto()
teine_auto.set_kiirus(100) # [km/h]
teine_auto.get_asukoht(2.0) # [h]
200.0
1.1.2 Klass Punkt ja selle objekti esitus sõnena¶
Mis vahet on maagilistel meetoditel __repr__ ja __str__, millal millist kasutada? Eelmine nädal mainisime, et mõlemad on objekti esitamiseks sõne kujul konsoolis ja/või mujal. Mis neil vahet on:
- Meetod
__repr__:- Kasutame koodi arendamise ajal.
- Esitab objekti andmeid detailsemalt.
- Vaikimisi juht kõigi sõne esituste jaoks.
- Kiire objekti info konsoolis järgivaatamiseks.
- Meetodile vastab funktsioon
repr.
- Meetod
__str__:- Kasutame objekti esitamiseks lõppkasutaja jaoks.
- Vähem tehnilisi andmeid, vähem detailsem.
- Kui meetod
__str__pole defineeritud/üle kirjutatud kasutatakse vaikimisi meetodi__repr__väljundit. - Meetodile vastavad funktsioonid
strjaprint.
Loome klassi Punkt ja demostreerime meetodite __repr__ ja __str__ kasutuse:
class Punkt:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self): # Meetodile vastab funtsioon repr.
return f'<< {self.x}, {self.y} >>'
def __str__(self): # Kautamiseks funktsiooniga str ja print.
return f'< {self.x}, {self.y} >'
def __add__(self, other): # Argument other on ka punkt.
summa_x = self.x + other.x # Eeldan, et other'il muutuja x olemas.
summa_y = self.y + other.y
return Punkt(summa_x, summa_y) # Loome uue objekti.
#return type(self)(summa_x, summa_y) # Nii saab ka.
p1 = Punkt(1, 1) # Loon kaks punkti.
p2 = Punkt(2, 2)
print(p1)
print(p2)
uus_p = p1 + p2
print(type(uus_p))
print(str(uus_p))
print(uus_p)
print(repr(uus_p))
uus_p
< 1, 1 > < 2, 2 > <class '__main__.Punkt'> < 3, 3 > < 3, 3 > << 3, 3 >>
<< 3, 3 >>
Pane tähele, et meetod __add__ väljastab tulemuse andmetüübina Punkt. See võimaldab muuseas järgmist käitumist:
p3 = Punkt(1, 1) # Loon veel ühe punkti.
print(p3)
uus_p = p1 + p2 + p3
print(type(uus_p))
print(str(uus_p))
print(uus_p)
print(repr(uus_p))
uus_p
< 1, 1 > <class '__main__.Punkt'> < 4, 4 > < 4, 4 > << 4, 4 >>
<< 4, 4 >>
Siin interpreteeritakse koodi ülevalt alla ja vasakult paremale. Tehte p1 + p2 vastuse andmetüüp on Punkt. Peale p1 + p2 leidmise, liidetakse leitud punktile punkt p3 ja selle tehte vastuse andmetüüp on sammuti Punkt. Kui meetod __add__ ei tagastaks punkti vaid sõne mis sisaldab kahte arvu siis teine liitmistehe poleks õnnestunud (Erisus TypeError), vrd. järgmine alampunkt.
1.1.3 Klass Punkt ja selle objekti vaikimisi esitus¶
Mis saab kui eelnevad soovitusi eirata:
class Punkt:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # Argument other on ka punkt.
summa_x = self.x + other.x # Eeldan, et other'il muutuja x olemas.
summa_y = self.y + other.y
return f'< {summa_x}, {summa_y} >'
p1 = Punkt(1, 1) # Loon kaks punkti.
p2 = Punkt(2, 2)
print(p1)
print(p2)
uus_p = p1 + p2
print(type(uus_p))
print(str(uus_p))
print(uus_p)
print(repr(uus_p))
uus_p
<__main__.Punkt object at 0x104fada10> <__main__.Punkt object at 0x104fb2a10> <class 'str'> < 3, 3 > < 3, 3 > '< 3, 3 >'
'< 3, 3 >'
Siin pole rohkem kui ühe arvutuse tegemine toetatud. Tõstatub erisus TypeError:
p3 = Punkt(1, 1) # Loon veel ühe punkti.
print(p3)
uus_p = p1 + p2 + p3
<__main__.Punkt object at 0x104fa4e90>
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[6], line 4 1 p3 = Punkt(1, 1) # Loon veel ühe punkti. 2 print(p3) ----> 4 uus_p = p1 + p2 + p3 TypeError: can only concatenate str (not "Punkt") to str
2 Sisseehitatud meetodite dekoraatorid¶
Eelmise nädal loengus nägime kuidas defineerida enda kirjutatud meetodeid dekoraatorit ja kuidas meetodit dekoreerida. See oli sarnane tavalise funktsiooni dekoreerimisega, vrd. Loeng 8. Dekoraator muudab meetodi käitumist ilma meetodi koodi muutmata.
Klassides kasutamiseks on loodud sisseehitatud dekoraatorid millega saame dekoreerida klassi meetodeid.
Valik sisseehitatud meetodite dekoraatoreid:
property- Dünaamiline objektimuutuja või dünaamiline atribuut: Meetod käitub nagu objektimuutuja, väljakutse kujul<objekt>.<meetodi nimi>(sulud puuduvad).classmethod- Klassimeetod: Meetod mis ei sõltu objektist (self). Saame endiselt meetodi välja kutsuda kujul<Klassi nimi>.<meetodi nimi>()ja jääb ka võimalus teha väljakutset läbi objekti, kujul<objekt>.<meetodi nimi>(). Annab juurdepääsu mõjutada klassimuutujaid mis teatavasti kehtivad kogu klassile (kõik objektid, kõik meetodid). Meetodi defineerimisel peame kasutama argumeticlsesimese positsionaalse argumendina.staticmethod- Staatiline meetod: Sõltumatu nii klassist (cls) kui ka tema objektidest (self).
Märkus 1: Pythonis esineb ka muid klassis kasutamiseks mõeldud dekoraatoreid. Siin kursuses vaatleme kolme eelmainitud.
Märkus 2: Selles kursuses käsitleme dekoraatorit property ja selle poolt loodud muutujaid lihtsustatud kujul. Tudengid kes on huvitatud täielikumast ülevaatest, lugege järgmist: https://docs.python.org/3/library/functions.html#property
2.1 Meetodi dekoraator property¶
Meetodi dekoraatori property kasutamise näide ehk dünaamilise objektimuutuja loomine:
class Auto:
def __init__(self, mudel, hind):
self.mudel = mudel # Objektimuutujad.
self.hind = hind
@property
def mudel_ja_hind(self): # Dünaamiline objektimuutuja.
return self.mudel, self.hind
auto = Auto('BMW', 4000) # Loon objekti auto.
print(auto.mudel_ja_hind) # Meetod mis käitub nagu objektimuutuja.
('BMW', 4000)
Kuna tegu on dekoreeritud meetodiga, siis väljakutse kujul auto.mudel_ja_hind() ja Auto.mudel_ja_hind(auto) pole enam võimalikud:
auto.mudel_ja_hind()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[8], line 1 ----> 1 auto.mudel_ja_hind() TypeError: 'tuple' object is not callable
print(Auto.mudel_ja_hind(auto))
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[9], line 1 ----> 1 print(Auto.mudel_ja_hind(auto)) TypeError: 'property' object is not callable
Märkus: Dekoraatorit property kasutatakse tavaliselt koos nn. getter, setter ja deleter'ga kujul @mudel_ja_hind.setter, @mudel_ja_hind.getter ja @mudel_ja_hind.deleter. Uuri kasutust iseseisvalt. Eelmainitud dekoraatorid lubavad nende poolt dekoreetitud meetodite abil juhtida objektimuutujate muutmist globaalses skoobis sarnaselt tavalistele muutujatele. Näiteks, kui meetod mida on dekoreeritud dekoraatriga mudel_ja_hind.setter on defineeritud, siis kood kujul auto.mudel_ja_hind = ('Ford', 300) kirjutab üle dünaamilise objektimuutuja mudel_ja_hind, jne.
2.2 Meetodi dekoraator classmethod¶
Meetodi dekoraatori classmethod kasutamise näide:
class Auto:
allahindlus = 0.95 # Klassimuutuja.
def __init__(self, mudel, hind):
self.mudel = mudel # Objektimuutujad.
self.hind = hind
def alanda_hinda(self):
self.hind = Auto.allahindlus * self.hind
@classmethod
def allahindlus_maar(cls, protsent): # NB! cls.
cls.allahindlus = protsent # Staatiline klassimuutuja.
auto = Auto('BMW', 4000)
print(Auto.allahindlus) # Klassimuutuja järelevaatamine, otse.
print(auto.allahindlus) # Klassimuutuja, läbi loodud objekti.
Auto.allahindlus_maar(0.9) # Uus allahindlusmäär, kehtib kogu klassile.
print(Auto.allahindlus) # Klassimuutuja järelevaatamine, otse.
print(auto.allahindlus) # Klassimuutuja, läbi loodud objekti.
auto.alanda_hinda() # Hinna alandamine.
print(auto.hind) # Uus hind.
teine_auto = Auto('Audi', 2000)
print(teine_auto.allahindlus) # Klassimuutuja, läbi uue objekti.
0.95 0.95 0.9 0.9 3600.0 0.9
2.3 Meetodi dekoraator staticmethod¶
Meetodi dekoraatori staticmethod kasutamise näide:
class Auto:
def __init__(self, mudel, hind):
self.mudel = mudel
self.hind = hind
@staticmethod
def on_lahe_auto(mudelnimi): # Ei sõltu klassist cls ega objektist self.
if mudelnimi == 'BMW':
return True
else:
return False
auto = Auto('BMW', 4000)
print(Auto.on_lahe_auto('Ford')) # Väljakutse läbi klassi.
print(Auto.on_lahe_auto('BMW')) # Väljakutse läbi klassi.
print(auto.on_lahe_auto('Ford')) # Väljakutse läbi objekti.
print(auto.on_lahe_auto('BMW')) # Väljakutse läbi objekti.
False True False True
3.1 Mitmest superklassist pärimine¶
Pythoni alamklass on võimeline pärima mitmest superklassist korraga. Superklasside arv on meelevaldne. Näide kasutades pärimisel kahte superklassi:
class A:
cls_A = 1
def cls_A_method(self):
print('Class A method.')
class B:
cls_B = 2
def cls_B_method(self):
print('Class B method.')
class C(A, B): # Pärimine klassist A ja siis klassist B.
cls_C = 3
def cls_C_method(self):
print('Class C method.')
# Väljakutsed kasutades alamklassi C.
print(C.cls_A) # Päritud.
print(C.cls_B) # Päritud.
print(C.cls_C)
cls_C_obj = C() # Objekti loomine.
C.cls_A_method(cls_C_obj) # Päritud.
C.cls_B_method(cls_C_obj) # Päritud.
C.cls_C_method(cls_C_obj)
# Väljakutsed kasutades klassi C objekti.
print(cls_C_obj.cls_A) # Päritud.
print(cls_C_obj.cls_B) # Päritud.
print(cls_C_obj.cls_C)
cls_C_obj.cls_A_method() # Päritud.
cls_C_obj.cls_B_method() # Päritud.
cls_C_obj.cls_C_method()
1 2 3 Class A method. Class B method. Class C method. 1 2 3 Class A method. Class B method. Class C method.
Päritud atribuute saab loomulikult üle kirjutada:
class A:
cls_A = 1
def cls_A_method(self):
print('Class A method.')
class B:
cls_B = 2
def cls_B_method(self):
print('Class B method.')
class C(A, B): # Pärimine klassist A ja B.
cls_A = 111 # Kasutan nime, muudan sisu.
def cls_B_method(self): # Kasutan nime muudan sisu.
print('Klassi B ülekirjutatud meetod.')
# Väljakutsed kasutades alamklassi C.
print(C.cls_A) # Päritud ja üle kirjutatud.
print(C.cls_B) # Päritud.
cls_C_obj = C() # Objekti loomine.
C.cls_A_method(cls_C_obj) # Päritud.
C.cls_B_method(cls_C_obj) # Päritud ja üle kirjutatud.
# Väljakutsed kasutades klassi C objekti.
print(cls_C_obj.cls_A) # Päritud ja üle kirjutatud.
print(cls_C_obj.cls_B) # Päritud.
cls_C_obj.cls_A_method() # Päritud.
cls_C_obj.cls_B_method() # Päritud ja üle kirjutatud.
111 2 Class A method. Klassi B ülekirjutatud meetod. 111 2 Class A method. Klassi B ülekirjutatud meetod.
Mida ütleb method resolution order (MRO)?
C.mro()
[__main__.C, __main__.A, __main__.B, object]
C.__mro__
(__main__.C, __main__.A, __main__.B, object)
Seega sulgudesse paigutatud superklasside järjekord on oluline.
3.2 Mitmest superklassist pärimine ja transitiivsus¶
Päritavad superklassid saavad omakorda pärida kolmandatest superklassidest jne. Allolevas näites olevate superklasside sisu (klassimuutujad, objektimuutjad ja meetodid) pärandatakse alamklassile:
class A:
cls_A = 1
def cls_A_method(self):
print('Class A method.')
class B(A): # Pärib klassi A sisu.
cls_B = 2
def cls_B_method(self):
print('Class B method.')
class C(B): # Pärib klassi B sisu, mis omakorda päris klassi A sisu.
cls_C = 3
def cls_C_method(self):
print('Class C method.')
# Väljakutsed kasutades alamklassi C.
print(C.cls_A) # Päritud klassist A läbi klassi B.
print(C.cls_B) # Päritud klassist B.
print(C.cls_C)
cls_C_obj = C() # Objekti loomine.
C.cls_A_method(cls_C_obj) # Päritud klassist A läbi klassi B.
C.cls_B_method(cls_C_obj) # Päritud klassist B.
C.cls_C_method(cls_C_obj)
# Väljakutsed kasutades alamklassi C objekti.
print(cls_C_obj.cls_A) # Päritud klassist A läbi klassi B.
print(cls_C_obj.cls_B) # Päritud klassist B.
print(cls_C_obj.cls_C)
cls_C_obj.cls_A_method() # Päritud klassist A läbi klassi B.
cls_C_obj.cls_B_method() # Päritud klassist B.
cls_C_obj.cls_C_method()
1 2 3 Class A method. Class B method. Class C method. 1 2 3 Class A method. Class B method. Class C method.
Pärimise omadust kus alamklass D pärib mitmest superklassist (A, B, C) järgmiselt: A $\to$ B(A) $\to$ C(B) $\to$ D(C), nimetatakse transitiivsuseks. Eelmise lauses näites, saab pärimiste järjestikune jada olla meelevaldselt pikk.
3.3 Konstruktori pärimine, ülekirjutamine, funktsioon super ja alamklassi objekti initsialiseeringu laiendamine ning ahendamine¶
Klassid saavad pärida teiste klasside kõiki atribuute (klassimuutujad, objektimuutujad, meetodid, jm.) k.a. initsialiseeringuid mis on defineeritud maagilises meetodis __init__ ehk konstruktoris. Seda tehakse siis kui on soov objekti laiendada või ahendada.
3.3.1 Konstruktori pärimine¶
Pärimisel kasutades süntaksit AlamKlass(SuperKlass) päritakse superklassist kõik k.a. konstruktor. Kui sul pole vaja alamklassi initsialiseeringut muuta siis pole seda mõtet ka üle kirjutada:
class Inimene: # Superklass.
def __init__(self, nimi, perenimi, sugu):
self.nimi = nimi
self.perenimi = perenimi
self.sugu = sugu
class Tudeng(Inimene): # Alamklass.
pass
Juulia = Inimene('Juulia', 'Tipikas', 'naine')
Juulius = Tudeng('Juulius', 'Tipikas', 'mees')
print(Juulia.sugu)
print(Juulius.sugu)
naine mees
3.3.2 Konstruktori ülekirjutamine kasutades superklassi nime¶
Näide milles päritakse ja kirjutatakse üle konstruktor ehk maagiline meetod __init__ ilma selle andmete struktuuri ja sisu muutmata. Me teame, et saame kasutada klassi nime viitamaks selle meetoditele, seega:
class Inimene: # Superklass.
def __init__(self, nimi, perenimi, sugu):
self.nimi = nimi
self.perenimi = perenimi
self.sugu = sugu
class Tudeng(Inimene): # Alamklass.
def __init__(self, nimi, perenimi, sugu):
# Viitan konstruktorile läbi superklassi nime.
Inimene.__init__(self, nimi, perenimi, sugu) # NB! Argument self.
Juulia = Inimene('Juulia', 'Tipikas', 'naine')
Juulius = Tudeng('Juulius', 'Tipikas', 'mees')
print(Juulia.sugu)
print(Juulius.sugu)
naine mees
3.3.3 Konstruktori ülekirjutamine ja sisseehitatud funktsioon super¶
Sisseehitatud funktsioon super võimaldab viidata superklassi meetoditele alamklassi seest, k.a. konstruktorile ilma, et sa peaksid klassi nimele ilmutatud kujul viitama seda nimepidi kutsudes. Funktsiooni super kasutame peamiselt initsialiseeringutele viitamiseks. Lisaks kasutatakse seda ka transitiivsel konstruktorite pärimisel ülevaate ja lihtsuse tagamiseks.
class Inimene: # Superklass.
def __init__(self, nimi, perenimi, sugu):
self.nimi = nimi
self.perenimi = perenimi
self.sugu = sugu
class Tudeng(Inimene): # Alamklass
def __init__(self, nimi, perenimi, sugu):
super().__init__(nimi, perenimi, sugu) # NB! Argument self puudub.
Juulia = Inimene('Juulia', 'Tipikas', 'naine')
Juulius = Tudeng('Juulius', 'Tipikas', 'mees')
print(Juulia.sugu)
print(Juulius.sugu)
naine mees
3.3.4 Konstruktori ülekirjutamine ja alamklassi objekti laiendamine¶
Kui soovime alamklassis objekti initsialiseeringut laiendada või muud moodi muuta, nt. kasutada uusi objekti muutujaid, saame seda teha järgmiselt:
- Initsialiseerime kasutades superklassi nime.
class Inimene:
def __init__(self, nimi, perenimi, sugu):
self.nimi = nimi
self.perenimi = perenimi
self.sugu = sugu
class Tudeng(Inimene):
def __init__(self, nimi, perenimi, sugu, iq):
Inimene.__init__(self, nimi, perenimi, sugu)
self.iq = iq # Lisan uue objekti muutuja.
Juulia = Inimene('Juulia', 'Tipikas', 'naine')
Juulius = Tudeng('Juulius', 'Tipikas', 'mees', 100)
print(Juulia.sugu)
print(Juulius.sugu)
print(Juulius.iq)
naine mees 100
- Initsialiseerime kasutades funktsiooni
super. Kasutades funktsioonisupersaame punktiga 1 sama tulemuse.
class Inimene:
def __init__(self, nimi, perenimi, sugu):
self.nimi = nimi
self.perenimi = perenimi
self.sugu = sugu
class Tudeng(Inimene):
def __init__(self, nimi, perenimi, sugu, iq):
super().__init__(nimi, perenimi, sugu)
self.iq = iq # Lisan uue objekti muutuja.
Juulia = Inimene('Juulia', 'Tipikas', 'naine')
Juulius = Tudeng('Juulius', 'Tipikas', 'mees', 100)
print(Juulia.sugu)
print(Juulius.sugu)
print(Juulius.iq)
naine mees 100
3.3.5 Konstruktori ülekirjutamine ja alamklassi objekti ahendamine¶
Kui soovid objekti muutujaid vähendada siis on selleks kaks võimalust. Allolevates näidetes kasutame funktsiooni super.
- Vaikeväärtuse
Nonekasutamine superklassis (objektimuutuja jääb alles).
class Inimene:
def __init__(self, nimi, perenimi, sugu=None): # NB! Kasuta None.
self.nimi = nimi
self.perenimi = perenimi
self.sugu = sugu
class Tudeng(Inimene):
def __init__(self, nimi, perenimi):
super().__init__(nimi, perenimi) # Ei ole soost huvitatud.
Juulia = Inimene('Juulia', 'Tipikas', 'naine')
Juulius = Tudeng('Juulius', 'Tipikas')
print(Juulia.sugu)
print(Juulius.nimi)
print(Juulius.perenimi)
print(Juulius.sugu)
hasattr(Juulius, "sugu")
naine Juulius Tipikas None
True
- Kasutades vaikeväärtust
Noneja tingimusliku objektimuutuja loomist superklassis (objektimuutujat ei looda).
class Inimene:
def __init__(self, nimi, perenimi, sugu=None): # NB! None.
self.nimi = nimi
self.perenimi = perenimi
if sugu is not None:
self.sugu = sugu # Luuakse ainult siis kui vaja.
class Tudeng(Inimene):
def __init__(self, nimi, perenimi):
super().__init__(nimi, perenimi, sugu=None) # NB! None. Muutujat sugu ei looda.
Juulia = Inimene('Juulia', 'Tipikas', 'naine')
Juulius = Tudeng('Juulius', 'Tipikas')
print(Juulia.sugu)
print(Juulius.nimi)
print(Juulius.perenimi)
hasattr(Juulius, "sugu")
naine Juulius Tipikas
False
3.3.6 Superklassi meetodile viitamine alamklassi seest ja funktsioon super¶
Alamklassi sees saame teha selle teiste meetodite väljakutseid. Näiteks:
class Tudeng:
def __init__(self, nimi, perenimi, sugu):
self.nimi = nimi
self.perenimi = perenimi
def to_string(self):
print(f'Nimi on {self.nimi} {self.perenimi}.')
def meetod_1(self):
Tudeng.to_string(self)
def meetod_2(self):
self.to_string()
Juulius = Tudeng('Juulius', 'Tipikas', 'mees')
Juulius.meetod_1()
Juulius.meetod_2()
Nimi on Juulius Tipikas. Nimi on Juulius Tipikas.
Kas alamklassis on võimalik teha ka superklassi meetodite väljakutseid?
On küll. Lisame klassile Inimene meetodi to_string mis prindib objekti andmed. Pärime selle klassi alamklassi nimega Tudeng. Alamklassis Tudeng viitame meetodile to_string ja teeme selle väljakutse (võid ka midagi muud teha) kasutades superklassi nime ja funktsiooni super:
class Inimene:
def __init__(self, nimi, perenimi, sugu):
self.nimi = nimi
self.perenimi = perenimi
self.sugu = sugu
def to_string(self):
print(f'Nimi on {self.nimi} {self.perenimi}.')
class Tudeng(Inimene):
def __init__(self, nimi, perenimi, sugu):
super().__init__(nimi, perenimi, sugu)
def meetod_1(self):
Tudeng.to_string(self) # Viide meetodile kasutades superklassi nime.
def meetod_2(self):
super().to_string() # Viide meetodile kasutades funktsiooni super.
def meetod_3(self):
Tudeng.to_string(self) # Viide meetodile kasutades alamklassi nime.
def meetod_4(self):
self.to_string() # Viide meetodile läbi kohaliku objekti.
Juulius = Tudeng('Juulius', 'Tipikas', 'mees')
Juulius.to_string() # Superklassis defineeritud ja päritud.
Juulius.meetod_1() # Alamklassis defineeritud.
Juulius.meetod_2() # Alamklassis defineeritud.
Juulius.meetod_3() # Alamklassis defineeritud.
Juulius.meetod_4() # Alamklassis defineeritud.
Nimi on Juulius Tipikas. Nimi on Juulius Tipikas. Nimi on Juulius Tipikas. Nimi on Juulius Tipikas. Nimi on Juulius Tipikas.
Lisaks näeme siit, et saame päritud superklassi meetodile to_string ligi kasutades ka kohalikku objekti self, alamklassi nime Tudeng ja superklassi nime Inimene.
4 Pesastatud klass¶
Klasse nii nagu kõiki muid Pythoni plokke on võimalik üksteise sisse pesastada. All on näide klassist mis defineerib objekti tudeng kellega on seotud objekt mobiiltelefon ning kes saab sellega (meetodid) uhkustada ja helistada etteantud numbrile.
Märkus: Alloleva näite saab lahendada ka kasutades kahte eraldi klassi.
class Tudeng:
def __init__(self, nimi, perenimi):
self.nimi = nimi
self.perenimi = perenimi
self.mobiiltelefon = self.Mobiil() # Loon objekti viidates klassile Mobiil. NB! sulud.
class Mobiil:
def __init__(self):
self.tootja = "LG"
self.mudel = 'LM50PM'
self.number = 12345678
def uhkusta(self):
return 'Mul on {} {} ja minu telefoni number on {}.'.format(self.tootja, self.mudel, self.number)
def helista(self, number):
print('Helistamine numbrile {} toimub.'.format(number))
tudeng = Tudeng('Juulius', 'Tipkas')
print(tudeng.mobiiltelefon.tootja)
print(tudeng.mobiiltelefon.mudel)
print(tudeng.mobiiltelefon.number)
tudeng.mobiiltelefon.helista(112) # Läbi konstruktori.
print(tudeng.mobiiltelefon.uhkusta()) # Läbi konstruktori.
mobiil = Tudeng.Mobiil() # Pesastatud klassiga objekti loomine.
print(mobiil)
LG LM50PM 12345678 Helistamine numbrile 112 toimub. Mul on LG LM50PM ja minu telefoni number on 12345678. <__main__.Tudeng.Mobiil object at 0x105d09e10>
Vajadusel võime pesastatud meetodite väljakutseid lühendada luues pesastava klassi skoopi meetodid mis viitavad pesastatud klassi meetoditele:
class Tudeng:
def __init__(self, nimi, perenimi):
self.nimi = nimi
self.perenimi = perenimi
self.mobiiltelefon = self.Mobiil()
def uhkusta(self): # Tekitasin viite pesastatud meetodi objektile.
return self.mobiiltelefon.uhkusta()
def helista(self, num): # Tekitasin viite pesastatud meetodile.
return self.mobiiltelefon.helista(num)
class Mobiil:
def __init__(self):
self.tootja = "LG"
self.mudel = 'LM50PM'
self.number = 12345678
def uhkusta(self):
return 'Mul on {} {} ja minu telefoni number on {}.'.format(self.tootja, self.mudel, self.number)
def helista(self, number):
print('Helistamine numbrile {} toimub.'.format(number))
tudeng = Tudeng('Juulius', 'Tipkas')
print(tudeng.mobiiltelefon.tootja)
print(tudeng.mobiiltelefon.mudel)
print(tudeng.mobiiltelefon.number)
tudeng.mobiiltelefon.helista(112) # Läbi konstruktori.
tudeng.helista(911) # Taustal kasutame loodud viidet.
print(tudeng.mobiiltelefon.uhkusta()) # Läbi konstruktori.
print(tudeng.uhkusta()) # Taustal kasutame loodud viidet.
LG LM50PM 12345678 Helistamine numbrile 112 toimub. Helistamine numbrile 911 toimub. Mul on LG LM50PM ja minu telefoni number on 12345678. Mul on LG LM50PM ja minu telefoni number on 12345678.
5 Klass ja kapseldamine¶
Teatavasti saame klassi- ja objektimuutujatele ligi globaalsest skoobis kuna class-plokk ei lokaliseeri muutujaid (see pole def-plokk). Sellist käitumist saab muuta kapseldades muutujaid ja meetodeid kasutades nn. nime moonutamist või nime varjamist. Kapseldatud nimesid saab kasutada ainult klassi ploki sees.
Nime moonutus (name mangling) on mehhanism, mille abil Python muudab klassi sees defineeritud topeltalajoonelise (dunder) nime (__nimi) sisemise identifikaatori kujule _KlassiNimi__nimi. Selle protsessi eesmärk on takistada nimede juhuslikku kokkupõrget alamklasside nimeruumidega ning toetada kapseldamist ilma range juurdepääsukontrollita.
Nime moonutatud (name mangling) nimed on peidetud globaalse skoobi eest. Seega see võte lubab klassi- ja objektimuutujaid ning meetodeid lokaliseerida (kaitsta).
Seega nime moonutamine on mõeldud:
- Nime konfliktide ja nimede ülekirjutamise kaitseks alamklassides.
- Saatmaks koodi lugejale signaali "ära puutu".
- Pakkumaks nõrka lokaliseeritust.
class A:
def __salajane(self):
print("Peidetud?")
a = A()
a._A__salajane()
A._A__salajane(a)
a.__salajane()
Peidetud? Peidetud?
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[28], line 8 6 a._A__salajane() 7 A._A__salajane(a) ----> 8 a.__salajane() AttributeError: 'A' object has no attribute '__salajane'
Kui sa tead kuidas nimi on moonutatud (mangled) saad sa meetodile ikkagist ligi, väljakutse a._A__salajane() või A._A__salajane(a) kaudu. See tähendab, et meetod on endiselt ligipääsetav, kuid mitte kogemata, ning alamklassid ei saa sama nime kasutades seda üle kirjutada ilma teadliku viiteta moonutatud nimele.
5.1 Kapseldamata klassi- ja objektimuutujad ning meetodid¶
Näide kapseldamata klassimuutujatest, objekti muutujatest ja meetodist ehk avalikest klassimuutujatest, objektimuutujatest ja meetoditest:
class A:
cls_A_a = 1
cls_A_b = 22
def __init__(self, obj_a, obj_b):
self.obj_a = obj_a
self.obj_b = obj_b
def cls_A_method(self):
return 'obj_a = {} and obj_b = {}'.format(self.obj_a, self.obj_b)
obj = A(3, 44)
print(A.cls_A_b) # Klassimuutuja.
print(obj.obj_b) # Objektimuutuja.
print(obj.cls_A_method()) # Meetodi väljakutse.
print(A.cls_A_method(obj)) # Meetodi väljakutse läbi klassi.
22 44 obj_a = 3 and obj_b = 44 obj_a = 3 and obj_b = 44
Eelnäidatud kood on täiesti hea kui sa kodeerid enda jaoks ja kasutad klassi nagu sa oled seda enda peas ette kujutanud. Sinu programmi võib aga kasutada keegi teine. Kuid klassi kasutaja, kes pole klassi programmeerija mõtetega tuttav, võib käituda näiteks järgmiselt. Objekti ja meetodeid on suhteliselt lihtne lõhkuda:
class A:
cls_A_a = 1
cls_A_b = 22
def __init__(self, obj_a, obj_b):
self.obj_a = obj_a
self.obj_b = obj_b
def cls_A_method(self):
return 'obj_a = {} and obj_b = {}'.format(self.obj_a, self.obj_b)
obj = A(3, 44)
print(obj.cls_A_method())
# Saame klassi- ja objektimuutujad vabalt üle kirjutada.
A.cls_A_b = 'Häkitud number.'
obj.obj_b = 'Häkitud objektimuutuja.'
print(A.cls_A_b)
print(obj.obj_b)
# See omakorda muudab meetodite käitumise.
print(obj.cls_A_method())
# Võime meetodi hoopis hävitada.
obj.cls_A_method = 'Häkitud meetod.'
print(obj.cls_A_method) # Väljakutse kujul obj.cls_A_method() võimatu.
obj_a = 3 and obj_b = 44 Häkitud number. Häkitud objektimuutuja. obj_a = 3 and obj_b = Häkitud objektimuutuja. Häkitud meetod.
Selleks, et eelolevat olukorda vältida saame klassi atribuudid kapseldada ehk muuta need privaatseks eesmärgiga kaitsta neid globaalse skoobi juurdepääsu eest. Mäletatavasti kapseldav skoop lokaliseeris muutujad kasutamiseks ainult sellest skoobist.
5.2 Kapseldatud klassi- ja objektimuutujad ning meetodid¶
Klassi atribuute kapseldame kasutades kahte alakriipsu klassi- ja objektimuutuja või meetodi nime ees, nt. foo $\to$ __foo, bar() $\to$ __bar() jne.
class A:
cls_A_a = 1 # Avalik klassimuutuja.
__cls_A_b = 22 # Kapseldatud ehk privaatne klassimuutuja.
def __init__(self, obj_a, obj_b):
self.obj_a = obj_a # Avalik objektimuutuja.
self.__obj_b = obj_b # Kapseldatud ehk privaatne objektimuutuja.
def __cls_A_method(self): # Kapseldatud ehk privaatne meetod.
return 'obj_a = {} and obj_b = {}.'.format(self.obj_a, self.__obj_b)
def data(self): # Avalik meetod.
obj_b_id = id(self.__obj_b)
return 'obj_b = {}, obj_b id = {}.'.format(self.__obj_b, obj_b_id)
obj = A(3, 44)
print(obj.data())
obj_b = 44, obj_b id = 4305874320.
Globaalse skoobi väljakutsed järgmisel kujul on kaitstud, neid saab teostada sinult klassi ploki sees:
print(obj.__obj_b) # Juurdepääs puudub.
print(obj.__cls_A_method()) # Juurdepääs puudub.
print(A.__cls_A_method(obj)) # Juurdepääs puudub.
Nende väljakutsed tõstatavad erisuse AttributeError.
Näiteks:
print(obj.__obj_b)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[32], line 1 ----> 1 print(obj.__obj_b) AttributeError: 'A' object has no attribute '__obj_b'
Järgmised muutujaid ja meetodeid pole klassis defineeritud:
print(obj.obj_b) # Muutuja puudub.
print(obj.cls_A_method) # Meetod puudub.
print(obj.cls_A_method()) # Meetod puudub.
print(A.cls_A_b) # Muutuja puudub.
print(A.cls_A_method(obj)) # Meetod puudub.
Nende väljakutsed globaalses skoobis tõstatavad erisuse AttributeError õigustatult.
Näiteks:
print(obj.obj_b)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[33], line 1 ----> 1 print(obj.obj_b) AttributeError: 'A' object has no attribute 'obj_b'
NB! Kapseldatud atribuutidega klassi puhul näitab funktsioon dir ainult kapseldamata ehk avalikke atribuute, vt. loendi lõppu:
print(dir(A))
['_A__cls_A_b', '_A__cls_A_method', '__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__', 'cls_A_a', 'data']
Märkus: Kui püüad globaalses skoobis kapseldatud nimedega objekte üle kirjutada siis luuakse globaalses skoobis kehtivad uued objektid. Kontrollime ja vaatame kas globaalse skoobi ülekirjutatud objektimuutuja mälu aadress id(obj.__obj_b) väärtus erineb klassi sisesest aadressi väärtusest:
class A:
cls_A_a = 1 # Avalik klassimuutuja.
__cls_A_b = 22 # Kapseldatud ehk privaatne klassimuutuja.
def __init__(self, obj_a, obj_b):
self.obj_a = obj_a # Avalik objektimuutuja.
self.__obj_b = obj_b # Kapseldatud ehk privaatne objektimuutuja.
def __cls_A_method(self): # Kapseldatud ehk privaatne meetod.
return 'obj_a = {} and obj_b = {}.'.format(self.obj_a, self.__obj_b)
def data(self): # Avalik meetod.
obj_b_id = id(self.__obj_b)
return '__obj_b = {}, __obj_b id = {}.'.format(self.__obj_b, obj_b_id)
obj = A(3, 4)
print(obj.data()) # Kapseldatud muutuja __obj_b.
# Püüan kirjutada üle kapseldatud objektimuutuja.
obj.__obj_b = 'Häkitud __muutuja.' # Taustal luuakse uus objekt.
print(obj.__obj_b, 'id =', id(obj.__obj_b), 'erineb kapseldatud muutuja omast.')
__obj_b = 4, __obj_b id = 4305873040. Häkitud __muutuja. id = 5098544400 erineb kapseldatud muutuja omast.
Kapseldatud meetodi __cls_A_method kohta saab näidata sama käitumist:
print("Kapseldatud meetod id =", id(obj._A__cls_A_method))
# Püüan kirjutada üle kapseldatud meetodi.
obj.__cls_A_method = 'Häkitud __meetod.' # Taustal luuakse uus objekt.
print(obj.__cls_A_method, 'id =', id(obj.__cls_A_method), 'erineb kapseldatud muutuja omast.')
Kapseldatud meetod id = 5098872896 Häkitud __meetod. id = 5098552272 erineb kapseldatud muutuja omast.
Kapseldatud objektimuutuja on peidetud globaalse skoobi eest, veel üks näide:
class Auto:
def __init__(self):
self.__hind = 100
def müü_auto(self):
print(f"Auto müüdud hinnaga {self.__hind}.")
def määra_hind(self, hind):
self.__hind = hind
mersu = Auto()
# Müüme vaikimisi hinnaga.
mersu.müü_auto()
# Püüan muuta hinda ja müün.
mersu.__hind = 5000 # Ei kehti klassi sees.
mersu.müü_auto() # NB! Ei õnnestu!
# Määran uue hinna läbi meetodi määra_hind ja müün.
mersu.määra_hind(5000)
mersu.müü_auto()
Auto müüdud hinnaga 100. Auto müüdud hinnaga 100. Auto müüdud hinnaga 5000.
5.3 Kapseldamine pole päriselt lokaliseeriv¶
Nagu eespool väideti, pole kapseldamine lokaliseeriv kuna kapseldatud atribuudile pääseb ikkagist ligi kasutades süntaksit (teades konkreetset nime moonutust):
obj._KlassiNimi__meetodnimi()
või
obj._KlassiNimi__muutujanimi
class A:
def __salajane(self):
print("Peidetud?")
a = A()
print(hasattr(a, "_A__salajane"))
print(hasattr(a, "__salajane")) # Ei näe seda siit.
a._A__salajane() # Töötab.
a.__salajane() # Kaitstud: Erisus AttributeError.
True False Peidetud?
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[38], line 11 8 print(hasattr(a, "__salajane")) # Ei näe seda siit. 10 a._A__salajane() # Töötab. ---> 11 a.__salajane() AttributeError: 'A' object has no attribute '__salajane'
5.4 Üksiku alakriipsu kasutus nime ees¶
Pythoni objektorienteeritud süsteemis kasutatakse üksikut alakriipsu (_) atribuutide ja meetodite ees semantilise konventsioonina, millega tähistatakse kaitstud (protected) juurdepääsutasemega elemente. Kuigi Python ei piira juurdepääsu ega ei rakenda klassikalist kapseldust kompilaatori tasemel, on see tähistus laialdaselt aktsepteeritud kui osa keele stilistilisest ja semiootilisest kokkuleppest.
Seega süntaksiga kujul foo $\to$ _foo või bar() $\to$ _bar() vihjatakse lugejale, et neid atribuute tuleks kasutada ainult klasside sees. Näide:
class Auto:
def __init__(self, hind):
self._hind = hind
def müü_auto(self):
print(f"Auto müüdud hinnaga {self._hind}.")
def _määra_hind(self, hind):
self._hind = hind
mersu = Auto(100)
# Müüme vaikimisi hinnaga.
mersu.müü_auto()
# Muudan hinda ja müün.
mersu._hind = 5000 # Lubab üle kirjutada kuna sisulist kapseldamist ei toimu.
mersu.müü_auto() # Õnnestub!
# Määran uue hinna läbi meetodi _määra_hind ja müün.
mersu._määra_hind(5000) # Lubab üle kirjutada kuna sisulist kapseldamist ei toimu.
mersu.müü_auto() # Õnnestub!
Auto müüdud hinnaga 100. Auto müüdud hinnaga 5000. Auto müüdud hinnaga 5000.
6 Andmetüübi tüübiviide või tüübimärgend (type hint)¶
6.1 Tüübiviite mõiste ja milleks seda kasutatakse¶
Tüübiviited on üksnes annotatsioonid — metaandmed, mis kirjeldavad, milliseid andmetüüpe funktsioon või meetod eeldavad. Mis andmetüüpi peavad olema sisendid ja mis tüüpi saab olema väljund.
Märkus: Python ise ei kontrolli tüübimärgendeid automaatselt. Pythoni tüübimärgendid ei ole programmi täitmise ajal jõustatud.
Tüübimärgiseid kasutatakse järgmiste vahendite poolt ja kontekstides:
- Staatilised tüübikontrollijad, nagu
mypy,pyrightvõi IDE-des nt. PyCharm. - Linterid (linters) ja muud tööriistad, mis aitavad leida tüübivigu enne programmi käivitamist.
- Täitmisaegsed tüübikontrollijad, nagu
typeguard. - Programmi eelduste esitus ilmutatud ja inimloetaval kujul.
6.2 Tüübiviite süntaks¶
Andmetüübile viitamiseks kasutame andmetüübi nime (objekti defineeriva klassi nime). Funktsiooni või meetodi argumendi andmetüüpi tähistame kooloniga arg: int ja vastavaid väljundite andmetüüpe noolega -> float:
def fun(a: int, b: str) -> float:
vastus = 10 / 3
return vastus
def fun(a: int, b: str) -> tuple:
print(a, b)
return a, 'Juulius'
Kui funktsioon väljastab jada andmetüübi, nt. korteeži, mis sisaldab sõnesid (meelevaldne arv elemente):
def fun() -> tuple[str]:
return "ok", "ok", "ok"
Kui korteežis lõplik arv (kaks) elemente:
def fun() -> tuple[bool, str]:
return (True, "ok")
Kui argument või väljund toetavad mitut andmetüüpi siis kasutame hulga ühendi operaatorit |:
def fun(a: int | float, b: str) -> tuple[int | float, str]: # Union operator |.
return a, 'Juulius'
def fun(x: str) -> None | int:
if x == 'Juulius':
return 2
Argumendil saab olla vaikeväärtus:
def greet(name: str = "World") -> None:
print(f"Hello, {name}!")
def add_item(item: str, items: list[str] | None = None) -> list[str]:
return ['a', 'b', 'c']
Kui väljund on jada milles on lõplik arv elemente mis saabad olla kahte erinevat tüüpi:
def fun() -> tuple[bool | str, bool | str]:
# return (True, 'Juulius') # Nii saab ka.
return (Juulius, 'False')
Kui väljund on jada milles on meelevaldne arv elemente mis saabad olla kahte erinevat tüüpi:
def fun() -> tuple[bool | str, ...]:
return (True, "yes", False, "no")
Kui argument saab olla mistahes tüüpi kasutame baasklassi object:
def fun(x: object) -> None: # None kuna ei väljastata tüüpi vaid tegevust.
x.upper()
Etteteadmata arv positionaalsete argumentide *args andmetüüp:
def fun(*args: int) -> None:
for a in args:
print(a)
Etteteadmata arv vaikeväärtusega argumentide *args andmetüüp (väärtuste mitte võtmete tüüp):
def fun(**kwargs: str) -> None:
for key, value in kwargs.items():
print(key, value)
Kui lubame erinevat tüüpi väärtusi:
def fun(*args: int | float, **kwargs: int | float) -> None:
pass
Kasutus klassis: Konstruktori __init__ väljundi tüübimärgend peab olema -> None, kuna initsialiseerimismeetod ei tagasta väärtust.
class User:
a: str = 'Juulius'
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def greet(self, other: str) -> str:
return f"Hello, {other}! I'm {self.name}."
6.3 Tüübiviidete täitmisaegne kontroll¶
Kui on soov tüübiviiteid programmi täitmise ajal automaatselt kontrollida siis saab selleks kasutada moodulit typeguard (Anaconda installatsiooniga kaasas). Moodulis oleva dekoraator typechecked tõstatab erisused TypeCheckError (või TypeError) kui programm eksib eeldatud andmetüüpide vastu.
Märkus: Ei toota interaktiivses Pythoni konsoolis IPython. Seega ei tööta ka Jupyter Lab'is ja Jupyter Notebook'is. Salvesta kood .py faili.
6.3.1 Kasutamine funktsiooniga¶
Näide:
from typeguard import typechecked
@typechecked
def fun(a: int | float, b: str) -> tuple[int | float, str]:
print(a, b)
return a, b
fun(2, 'Juulius')
fun(2, 2) # Tõstatub erisus TypeCheckError.
6.3.2 Kasutamine klassis¶
Näide:
from typeguard import typechecked
@typechecked
class User:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def greet(self, other: str) -> str:
return f"Hello, {other}! I'm {self.name}."
def log_message(self, msg: str) -> None:
print(msg)
u = User("Juulius", 23)
print(u.greet("Rektor"))
u.log_message("Ups!")
u2 = User(2, 23) # Tõstatub erisus TypeError.
Lisas 2 leiad koodinäiteid mis teostavad tüübiviidete käsitis kontrollimist.