Klasshierarkier
Specialklasser för speciella ändamål
Ibland stöter man på en situation där man redan har definierat en klass, men sedan inser att man behöver speciella egenskaper i vissa, men inte alla, instanser av klassen. Och ibland inser man att man har definierat två stycken mycket liknande klasser med bara små skillnader. Som programmerare strävar vi efter att alltid upprepa oss så lite som möjligt, medan vi behåller tydlighet och läsbarhet. Så hur kan vi ta hänsyn till olika implementeringar av liknande objekt?
Låt oss ta en titt på två klassdefinitioner: Studerande
och Larare
. Getter- och sättar-metoder har utelämnats tills vidare för att hålla exemplet kort.
class Studerande:
def __init__(self, namn: str, id: str, epost: str, studiepoang: str):
self.namn = namn
self.id = id
self.epost = epost
self.studiepoang = studiepoang
class Larare:
def __init__(self, namn: str, epost: str, rum: str, larande_ar: int):
self.namn = namn
self.epost = epost
self.rum = rum
self.larande_ar = larande_ar
Även i ett avskalat exempel som ovan har vi redan en hel del upprepningar: båda klasserna innehåller attributen namn
och epost
. Det vore en bra idé att ha en enda attributdefinition, så att det räcker med en enda funktion för att redigera båda attributen.
Tänk dig till exempel att skolans e-postadress ändras. Alla adresser skulle behöva uppdateras. Vi skulle kunna skriva två separata versioner av i stort sett samma funktion:
def uppdatera_epost(s: Studerande):
s.epost = s.epost.replace(".com", ".edu")
def uppdatera_epost2(s: Larare):
s.epost = s.epost.replace(".com", ".edu")
Att skriva i stort sett samma sak två gånger är onödig upprepning, för att inte tala om att det fördubblar möjligheterna till fel. Det skulle vara en klar förbättring om vi kunde använda en enda funktion för att arbeta med instanser av båda klasserna.
Båda klasserna har också attribut som är unika för dem. Att bara kombinera alla attribut i en enda klass skulle innebära att alla instanser av klassen då skulle ha onödiga attribut, bara olika för olika instanser. Det verkar inte heller vara en idealisk situation.
Arv
Objektorienterade programmeringsspråk innehåller vanligtvis en teknik som kallas arv (eng. inheritance). En klass kan ärva egenskaper från en annan klass. Förutom dessa ärvda egenskaper kan en klass också innehålla egenskaper som är unika för den.
Med detta i åtanke är det rimligt att klasserna Larare
och Studerande
har en gemensam bas eller föräldraklass Person
:
class Person:
def __init__(self, namn: str, epost: str):
self.namn = namn
self.epost = epost
Den nya klassen innehåller de egenskaper som delas av de andra två klasserna. Nu kan Studerande
och Larare
ärva dessa egenskaper och dessutom lägga till sina egna. :
Syntaxen för arv innebär helt enkelt att basklassens namn läggs till inom parentes på rubrikraden:
class Person:
def __init__(self, namn: str, epost: str):
self.namn = namn
self.epost = epost
def uppdatera_epost_doman(self, ny_doman: str):
gammal_doman = self.epost.split("@")[1]
self.epost = self.epost.replace(gammal_doman, ny_doman)
class Studerande(Person):
def __init__(self, namn: str, id: str, epost: str, studiepoang: str):
self.namn = namn
self.id = id
self.epost = epost
self.studiepoang = studiepoang
class Larare(Person):
def __init__(self, namn: str, epost: str, rum: str, larande_ar: int):
self.namn = namn
self.epost = epost
self.rum = rum
self.larande_ar = larande_ar
# Test
if __name__ == "__main__":
sam = Studerande("Sam Studerande", "1234", "sam@example.com", 0)
sam.uppdatera_epost_doman("example.edu")
print(sam.epost)
lars = Larare("Lars Lärare", "lars@example.fi", "A123", 2)
lars.uppdatera_epost_doman("example.ex")
print(lars.epost)
Både Studerande
och Larare
ärver klassen Person
, så båda har de egenskaper som definieras i klassen Person, inklusive metoden uppdatera_epost_doman
. Samma metod fungerar för instanser av båda de härledda klasserna.
Låt oss titta på ett annat exempel. Vi har en Bokhylla
som ärver klassen BokLada
:
class Bok:
""" Klassen modellerar en enkel bok """
def __init__(self, namn: str, forfattare: str):
self.namn = namn
self.forfattare = forfattare
class BokLada:
""" Klassen modellerar en låda för böcker """
def __init__(self):
self.bocker = []
def tillsatt_bok(self, bok: Bok):
self.bocker.append(bok)
def lista_bocker(self):
for bok in self.bocker:
print(f"{bok.namn} ({bok.forfattare})")
class Bokhylla(BokLada):
""" Klassen modellerar en hylla för böcker """
def __init__(self):
super().__init__()
def tillsatt_bok(self, bok: Bok, paikka: int):
self.bocker.insert(paikka, bok)
Klassen Bokhylla
innehåller metoden tillsatt_bok
. En metod med samma namn finns definierad i basklassen BokLada
. Detta kallas överskuggning (eng. overriding): om en härledd klass har en metod med samma namn som basklassen, överskuggar den härledda versionen originalet i instanser av den härledda klassen.
Tanken i exemplet ovan är att en ny bok som läggs till i en Bok låda alltid hamnar högst upp, men med en Bokhylla kan du själv ange platsen. Metoden lista_bocker
fungerar likadant för båda klasserna, eftersom det inte finns någon överordnad metod i den härledda klassen.
Låt oss prova dessa klasser:
if __name__ == "__main__":
# Vi skapar några böcker för testning
b1 = Bok("7 bröder", "Aleksis Kivi")
b2 = Bok("Sinuhe", "Mika Waltari")
b3 = Bok("Okänd soldat", "Väinö Linna")
# Vi skapar en BokLada och tillsätter böckerna
lada = BokLada()
lada.tillsatt_bok(b1)
lada.tillsatt_bok(b2)
lada.tillsatt_bok(b3)
# Vi skapar en Bokhylla och tillsätter böckerna (alltid till början av hyllan)
hylla = Bokhylla()
hylla.tillsatt_bok(b1, 0)
hylla.tillsatt_bok(b2, 0)
hylla.tillsatt_bok(b3, 0)
# Skriver ut
print("I lådan:")
lada.lista_bocker()
print()
print("I hyllan:")
hylla.lista_bocker()
I lådan: 7 bröder (Aleksis Kivi) Sinuhe (Mika Waltari) Okänd soldat (Väinö Linna)
I hyllan: Okänd soldat (Väinö Linna) Sinuhe (Mika Waltari) 7 bröder (Aleksis Kivi)
Klassen Bokhylla har alltså också tillgång till metoden lista_bocker
. Genom ärvning är metoden medlem i alla klasser som kommer från klassen BokLada
.
Arv och räckvidd av egenskaper
En härledd klass ärver alla egenskaper från sin basklass. Dessa egenskaper är direkt åtkomliga i den härledda klassen, såvida de inte har definierats som privata i basklassen (med två understreck före egenskapens namn).
Eftersom attributen för en Bokhylla är identiska med en BokLada, fanns det ingen anledning att skriva om konstruktorn för Bokhylla. Vi anropade helt enkelt basklassens konstruktor:
class Bokhylla(BokLada):
def __init__(self):
super().__init__()
Alla egenskaper i basklassen kan nås från den härledda klassen med funktionen super()
. Argumentet self
utelämnas från metodanropet, eftersom Python lägger till det automatiskt.
Men vad händer om attributen inte är identiska; kan vi fortfarande använda basklassens konstruktor på något sätt? Låt oss titta på en klass som heter Avhandling
och som ärver klassen Bok
. Den härledda klassen kan fortfarande anropa konstruktören från basklassen:
class Bok:
""" Klassen modellerar en enkel bok """
def __init__(self, namn: str, forfattare: str):
self.namn = namn
self.forfattare = forfattare
class Avhandling(Bok):
""" Klassen modellerar en magisteravhandling """
def __init__(self, namn: str, forfattare: str, vitsord: int):
super().__init__(namn, forfattare)
self.vitsord = vitsord
Konstruktorn i Avhandling
-klassen anropar konstruktorn i basklassen Bok
med argumenten för namn
och forfattare
. Dessutom anger konstruktören i den härledda klassen värdet för attributet vitsord
. Detta kan naturligtvis inte vara en del av basklassens konstruktor, eftersom basklassen inte har något sådant attribut.
Ovanstående klass kan användas på följande sätt:
# Testar
if __name__ == "__main__":
avhandling = Avhandling("Python och Universum", "Peter Python", 3)
# Skriv ut attributens värden
print(avhandling.namn)
print(avhandling.forfattare)
print(avhandling.vitsord)
Python och Universum Peter Python 3
Även om en härledd klass överskuggar en metod i sin basklass kan den härledda klassen fortfarande anropa den åsidosatta metoden i basklassen. I följande exempel har vi ett grundläggande Bonuskort
och ett särskilt Platinumkort
för särskilt lojala kunder. Metoden rakna_bonus
är åsidosatt i den härledda klassen, men den åsidosatta metoden anropar basmetoden:
class Produkt:
def __init__(self, namn: str, pris: float):
self.namn = namn
self.pris = pris
class Bonuskort:
def __init__(self):
self.kopta_produkter = []
def tillsatt_produkt(self, produkt: Produkt):
self.kopta_produkter.append(produkt)
def rakna_bonus(self):
bonus = 0
for produkt in self.kopta_produkter:
bonus += produkt.pris * 0.05
return bonus
class Platinumkort(Bonuskort):
def __init__(self):
super().__init__()
def rakna_bonus(self):
# Anropar metoden i basklassen...
bonus = super().rakna_bonus()
# ...och tillsätter ännu fem procent till totalet
bonus = bonus * 1.05
return bonus
Bonusen för ett Platinumkort beräknas alltså genom att anropa den överskuggade metoden i basklassen och sedan lägga till 5 procent extra till basresultatet. Ett exempel på hur dessa klasser används:
if __name__ == "__main__":
kort = Bonuskort()
kort.tillsatt_produkt(Produkt("Bananer", 6.50))
kort.tillsatt_produkt(Produkt("Mandariner", 7.95))
bonus = kort.rakna_bonus()
kort2 = Platinumkort()
kort2.tillsatt_produkt(Produkt("Bananer", 6.50))
kort2.tillsatt_produkt(Produkt("Mandariner", 7.95))
bonus2 = kort2.rakna_bonus()
print(bonus)
print(bonus2)
0.7225 0.7586250000000001
Se dina poäng genom att klicka på cirkeln nere till höger av sidan.