Tilbake
1.5
Polymorfisme og abstraksjon

1.5 Polymorfisme og abstraksjon

Polymorfisme, abstrakte klasser og grensesnitt.

65 min
6 oppgaver
PolymorfismeAbstraksjonGrensesnittDuck typing
Du leser den tradisjonelle versjonen
Din fremgang i kapitlet
0 / 6 oppgaver

Polymorfisme og abstraksjon

I førre kapittel lærte du om arv – korleis klassar kan byggje vidare på kvarandre. No skal vi ta dette eit steg vidare og utforske to av dei kraftigaste konsepta i objektorientert programmering: polymorfisme og abstraksjon.

Polymorfisme tyder bokstaveleg talt «mange former». I programmering tyder det at ulike objekt kan brukast på same måte, sjølv om dei oppfører seg forskjellig. Tenk på ein universalfjernkontroll: han har ein «spel av»-knapp som fungerer uansett om du bruker han til ein TV, ein DVD-spelar eller ein spelkonsoll. Knappen er den same, men resultatet er forskjellig.

Abstraksjon handlar om å definere kva noko skal gjere, utan å bestemme korleis det vert gjort. Det er som ei stillingsskildring: ho seier kva jobben inneber, men ikkje nøyaktig korleis kvar tilsett utfører oppgåvene.

Polymorfisme
Polymorfisme (frå gresk: poly = mange, morph = form) er evna til at objekt av ulike klassar kan behandlast likt – til dømes ved å kalle same metode – men oppføre seg forskjellig.

Det finst to hovudformer for polymorfisme i Python:

- Duck typing: Dersom eit objekt har rett metode, kan det brukast – uavhengig av klasse. «Dersom det går som ei and og kvakkar som ei and, er det ei and.»
- Metode-overstyring (override): Subklassar definerer sin eigen versjon av ein metode som finst i superklassen.

✏️Polymorfisme i praksis

Lat oss sjå på eit klassisk eksempel med geometriske former:

import math

class Form:
    def areal(self):
        return 0

    def beskrivelse(self):
        return "Ukjent form"

class Sirkel(Form):
    def __init__(self, radius):
        self.radius = radius

    def areal(self):
        return math.pi * self.radius ** 2

    def beskrivelse(self):
        return f"Sirkel med radius {self.radius}"

class Rektangel(Form):
    def __init__(self, lengde, bredde):
        self.lengde = lengde
        self.bredde = bredde

    def areal(self):
        return self.lengde * self.bredde

    def beskrivelse(self):
        return f"Rektangel ({self.lengde} x {self.bredde})"

class Trekant(Form):
    def __init__(self, grunnlinje, hoyde):
        self.grunnlinje = grunnlinje
        self.hoyde = hoyde

    def areal(self):
        return 0.5 * self.grunnlinje * self.hoyde

    def beskrivelse(self):
        return f"Trekant (grunnlinje={self.grunnlinje}, høyde={self.hoyde})"

# Polymorfisme: Behandle alle former likt!
former = [Sirkel(5), Rektangel(4, 6), Trekant(3, 8)]

for form in former:
    print(f"{form.beskrivelse()}: areal = {form.areal():.2f}")

Utskrift:

Sirkel med radius 5: areal = 78.54
Rektangel (4 x 6): areal = 24.00
Trekant (grunnlinje=3, høyde=8): areal = 12.00

Nøkkelpunkt: Vi kallar areal() og beskrivelse() på kvart objekt utan å bry oss om kva klasse det høyrer til. Kvar klasse har sin eigen implementasjon, men grensesnittet er det same.

Duck typing i Python

Python er eit dynamisk typa språk. Det tyder at Python ikkje sjekkar kva klasse eit objekt høyrer til – han sjekkar berre om objektet har metoden vi prøver å kalle. Dette vert kalla duck typing.

Namnet kjem frå uttrykket: «Dersom det går som ei and, sym som ei and og kvakkar som ei and, er det truleg ei and.»

I praksis tyder dette at du ikkje treng arv for å få polymorfisme i Python. Så lenge objekta har dei rette metodane, fungerer det.

✏️Duck typing

Her ser vi korleis duck typing fungerer utan arv:

class Hund:
    def lyd(self):
        return "Voff!"

class Katt:
    def lyd(self):
        return "Mjau!"

class Ku:
    def lyd(self):
        return "Møø!"

# Ingen felles superklasse – men alle har lyd()-metoden!
def dyrelyder(dyr_liste):
    for dyr in dyr_liste:
        print(dyr.lyd())

mine_dyr = [Hund(), Katt(), Ku()]
dyrelyder(mine_dyr)

Utskrift:

Voff!
Mjau!
Møø!

Funksjonen dyrelyder() bryr seg ikkje om kva klassar objekta høyrer til. Han spør berre: «Har du ein lyd()-metode?» Dersom ja, fungerer det!

Merk: Dersom vi sender inn eit objekt som ikkje har lyd(), får vi ein AttributeError. Det er risikoen med duck typing – feil vert oppdaga fyrst ved køyring.

Abstrakte klassar

Nokre gonger vil vi lage ein superklasse som aldri skal instansierast direkte. Han finst berre for å definere eit felles grensesnitt som subklassane implementere.

Til dømes: Det gir meining å lage ein Sirkel eller Rektangel, men ein generisk Form åleine gir ikkje meining – kva slags form er det? Vi kan bruke abstrakte klassar for å uttrykkje dette.

Python har modulen abc (Abstract Base Classes) som lèt oss definere abstrakte klassar og metodar.

Abstrakt klasse og abstrakt metode

- Abstrakt klasse: Ein klasse som ikkje kan instansierast direkte. Han fungerer som ein mal som subklassar må følgje.
- Abstrakt metode: Ein metode som er deklarert i den abstrakte klassen, men som ikkje har nokon implementasjon. Subklassar implementere (overstyre) alle abstrakte metodar.

I Python bruker vi dekoratoren @abstractmethod frå abc-modulen for å markere ein metode som abstrakt.

✏️Abstrakte klassar med abc
from abc import ABC, abstractmethod

class Form(ABC):
    """Abstrakt superklasse for geometriske former."""

    @abstractmethod
    def areal(self):
        """Beregn arealet. Alle subklasser MÅ implementere denne."""
        pass

    @abstractmethod
    def omkrets(self):
        """Beregn omkretsen. Alle subklasser MÅ implementere denne."""
        pass

    def vis_info(self):
        """Konkret metode – arves av alle subklasser."""
        print(f"Areal: {self.areal():.2f}")
        print(f"Omkrets: {self.omkrets():.2f}")

class Sirkel(Form):
    def __init__(self, radius):
        self.radius = radius

    def areal(self):
        import math
        return math.pi * self.radius ** 2

    def omkrets(self):
        import math
        return 2 * math.pi * self.radius

class Kvadrat(Form):
    def __init__(self, side):
        self.side = side

    def areal(self):
        return self.side ** 2

    def omkrets(self):
        return 4 * self.side

# Bruk:
s = Sirkel(5)
s.vis_info()
# Areal: 78.54
# Omkrets: 31.42

k = Kvadrat(4)
k.vis_info()
# Areal: 16.00
# Omkrets: 16.00

# Prøv å lage en Form direkte:
# form = Form()  # TypeError: Can't instantiate abstract class Form

Viktige poeng:
1. Form arvar frå ABC (Abstract Base Class) – dette gjer han abstrakt
2. areal() og omkrets() har @abstractmethod – subklassar implementere dei
3. vis_info() er ein vanleg metode som alle subklassar arvar
4. Du kan ikkje lage eit objekt av Form direkte – du får TypeError

✏️Manglande implementasjon gir feil

Lat oss sjå kva som skjer når vi gløymer å implementere ein abstrakt metode:

from abc import ABC, abstractmethod

class Betaling(ABC):
    @abstractmethod
    def betal(self, belop):
        pass

    @abstractmethod
    def kvittering(self):
        pass

class Kortbetaling(Betaling):
    def __init__(self, kortnummer):
        self.kortnummer = kortnummer

    def betal(self, belop):
        print(f"Betaler {belop} kr med kort {self.kortnummer[-4:]}")

    # OBS: Vi glemte å implementere kvittering()!

# Dette gir feil:
# k = Kortbetaling("1234567890123456")
# TypeError: Can't instantiate abstract class Kortbetaling
# with abstract method kvittering

Python tvingar oss til å implementere alle abstrakte metodar. Dette er ein kraftig tryggleik – det garanterer at alle subklassar har den funksjonaliteten vi ventar.

Retta versjon:

class Kortbetaling(Betaling):
    def __init__(self, kortnummer):
        self.kortnummer = kortnummer

    def betal(self, belop):
        print(f"Betaler {belop} kr med kort {self.kortnummer[-4:]}")

    def kvittering(self):
        return f"Kvittering: Kortbetaling med kort ****{self.kortnummer[-4:]}"

No kan vi lage eit Kortbetaling-objekt utan feil.

Grensesnitt og protokollar i Python

I mange programmeringsspråk (som Java og C#) finst det eigne grensesnitt (interfaces) som definerer kva metodar ein klasse må ha. Python har ikkje eit eige interface-nøkkelord, men tilbyr to alternativ:

1. Abstrakte klassar (som vi nettopp har sett) – fungerer som grensesnitt
2. Protokollar (Protocol frå typing-modulen) – ei meir moderne tilnærming

Ein protokoll definerer eit sett med metodar som eit objekt må ha, utan å krevje arv. Det er ei formalisering av duck typing.

✏️Protokollar i Python
from typing import Protocol

class Lagrbar(Protocol):
    """Alle klasser som kan lagres må ha disse metodene."""

    def lagre(self, filnavn: str) -> None:
        ...

    def last_inn(self, filnavn: str) -> None:
        ...

class Dokument:
    def __init__(self, innhold: str):
        self.innhold = innhold

    def lagre(self, filnavn: str) -> None:
        print(f"Lagrer dokument til {filnavn}")

    def last_inn(self, filnavn: str) -> None:
        print(f"Laster dokument fra {filnavn}")

class Bilde:
    def __init__(self, bredde: int, hoyde: int):
        self.bredde = bredde
        self.hoyde = hoyde

    def lagre(self, filnavn: str) -> None:
        print(f"Lagrer bilde ({self.bredde}x{self.hoyde}) til {filnavn}")

    def last_inn(self, filnavn: str) -> None:
        print(f"Laster bilde fra {filnavn}")

def sikkerhetskopi(objekter: list[Lagrbar]) -> None:
    """Lagrer alle objekter som følger Lagrbar-protokollen."""
    for obj in objekter:
        obj.lagre("backup.dat")

# Fungerer – begge har lagre() og last_inn()
filer = [Dokument("Hei"), Bilde(800, 600)]
sikkerhetskopi(filer)

Skilnad frå abstrakte klassar:
- Protokollar krev ingen arv – klassen treng berre å ha rette metodar
- Protokollar er primært for typesjekkarar (som mypy), ikkje køyretidskontroll
- Abstrakte klassar gir feilmelding ved køyring, protokollar ved typesjekkinga

Open/lukka-prinsippet

Polymorfisme og abstraksjon gjer det mogleg å følgje eit viktig designprinsipp i programvareutvikling: open/lukka-prinsippet (Open/Closed Principle).

> Programvare skal vere open for utviding, men lukka for endring.

Kva tyder dette i praksis? Det tyder at vi skal kunne leggje til ny funksjonalitet (nye klassar) utan å endre eksisterande kode. Lat oss sjå eit eksempel.

✏️Open/lukka-prinsippet i praksis
Dårleg design – lukka for utviding:

# DÅRLIG: Må endre funksjonen for hver ny betalingstype
def prosesser_betaling(type, belop):
    if type == "kort":
        print(f"Betaler {belop} kr med kort")
    elif type == "vipps":
        print(f"Betaler {belop} kr med Vipps")
    elif type == "kontant":
        print(f"Betaler {belop} kr kontant")
    # Ny betalingstype? Må endre DENNE funksjonen...

Godt design – open for utviding:

from abc import ABC, abstractmethod

class Betaling(ABC):
    @abstractmethod
    def betal(self, belop):
        pass

class Kortbetaling(Betaling):
    def betal(self, belop):
        print(f"Betaler {belop} kr med kort")

class VippsBetaling(Betaling):
    def betal(self, belop):
        print(f"Betaler {belop} kr med Vipps")

class KontantBetaling(Betaling):
    def betal(self, belop):
        print(f"Betaler {belop} kr kontant")

def prosesser_betaling(betaling: Betaling, belop):
    betaling.betal(belop)

# Ny betalingstype? Bare lag en ny klasse!
class KryptoBetaling(Betaling):
    def betal(self, belop):
        print(f"Betaler {belop} kr med kryptovaluta")

# Ingen endring i eksisterende kode nødvendig:
prosesser_betaling(KryptoBetaling(), 500)

Skilnaden:
- I den dårlege versjonen må vi endre prosesser_betaling() kvar gong
- I den gode versjonen legg vi berre til ein ny klasse – eksisterande kode vert ikkje rørt

Polymorfisme med innebygde funksjonar

Polymorfisme er ikkje berre noko vi lagar sjølve – det er innebygd i Python! Mange av dei innebygde funksjonane til Python bruker polymorfisme via dunder-metodar (dobbel understrek, «double underscore»).

Til dømes bruker len() metoden __len__(), str() bruker __str__(), og + bruker __add__(). Ved å implementere desse i våre eigne klassar kan vi gjere objekta våre kompatible med dei innebygde funksjonane til Python.

✏️Dunder-metodar og polymorfisme
class Handlekurv:
    def __init__(self):
        self.varer = []

    def legg_til(self, vare, pris):
        self.varer.append({"vare": vare, "pris": pris})

    def __len__(self):
        """Gjør at len(kurv) fungerer."""
        return len(self.varer)

    def __str__(self):
        """Gjør at print(kurv) gir pen utskrift."""
        if not self.varer:
            return "Tom handlekurv"
        linjer = [f"  - {v['vare']}: {v['pris']} kr" for v in self.varer]
        total = sum(v['pris'] for v in self.varer)
        return "Handlekurv:\n" + "\n".join(linjer) + f"\n  Totalt: {total} kr"

    def __contains__(self, varenavn):
        """Gjør at 'vare' in kurv fungerer."""
        return any(v['vare'] == varenavn for v in self.varer)

kurv = Handlekurv()
kurv.legg_til("Melk", 25)
kurv.legg_til("Brød", 35)
kurv.legg_til("Ost", 55)

print(len(kurv))          # 3
print(kurv)               # Pen utskrift av handlekurven
print("Melk" in kurv)     # True
print("Juice" in kurv)    # False

Ved å implementere __len__, __str__ og __contains__ vert klassen vår integrert med dei innebygde funksjonane til Python. Dette er polymorfisme i praksis!

Oppsummering

I dette kapittelet har du lært:

- Polymorfisme: objekt av ulike klassar vert behandla likt.
- Duck typing: typen vert bestemt av oppførsel, ikkje klasse.
- Abstrakt klasse: kan ikkje instansierast, fungerer som mal.
- Open/lukka-prinsippet: open for utviding, lukka for endring.
- Dunder-metodar: gir polymorfisme med innebygde funksjonar.

Noekkelbegreper


BegrepForklaring
PolymorfismeObjekt av ulike klassar vert behandla likt
Duck typingType vert bestemt av oppførsel
Abstrakt klasseMal som ikkje kan instansierast direkte

Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.