Osa 10

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 överstyrning (eng. overriding): om en härledd klass har en metod med samma namn som basklassen, överstyr 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()
Exempelutskrift

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)
Exempelutskrift

Python och Universum Peter Python 3

Även om en härledd klass överstyr 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 överstyrda 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)
Exempelutskrift

0.7225 0.7586250000000001

Loading
Loading
Loading
Loading
Du har nått slutet av den här delen! Fortsätt till nästa del:

Se dina poäng genom att klicka på cirkeln nere till höger av sidan.