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 forrige kapittel lærte du om arv – hvordan klasser kan bygge videre på hverandre. Nå skal vi ta dette et steg videre og utforske to av de kraftigste konseptene i objektorientert programmering: polymorfisme og abstraksjon.

Polymorfisme betyr bokstavelig talt «mange former». I programmering betyr det at ulike objekter kan brukes på samme måte, selv om de oppfører seg forskjellig. Tenk på en universalfjernkontroll: den har en «spill av»-knapp som fungerer uansett om du bruker den til en TV, en DVD-spiller eller en spillkonsoll. Knappen er den samme, men resultatet er forskjellig.

Abstraksjon handler om å definere hva noe skal gjøre, uten å bestemme hvordan det gjøres. Det er som en stillingsbeskrivelse: den sier hva jobben innebærer, men ikke nøyaktig hvordan hver ansatt utfører oppgavene.

Polymorfisme
Polymorfisme (fra gresk: poly = mange, morph = form) er evnen til at objekter av ulike klasser kan behandles likt – for eksempel ved å kalle samme metode – men oppføre seg forskjellig.

Det finnes to hovedformer for polymorfisme i Python:

- Duck typing: Hvis et objekt har riktig metode, kan det brukes – uavhengig av klasse. «Hvis det går som en and og kvakker som en and, er det en and.»
- Metode-overstyring (override): Subklasser definerer sin egen versjon av en metode som finnes i superklassen.

✏️Polymorfisme i praksis

La oss se på et 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 kaller areal() og beskrivelse() på hvert objekt uten å bry oss om hvilken klasse det tilhører. Hver klasse har sin egen implementasjon, men grensesnittet er det samme.

Duck typing i Python

Python er et dynamisk typet språk. Det betyr at Python ikke sjekker hvilken klasse et objekt tilhører – den sjekker bare om objektet har metoden vi prøver å kalle. Dette kalles duck typing.

Navnet kommer fra uttrykket: «Hvis det går som en and, svømmer som en and og kvakker som en and, er det sannsynligvis en and.»

I praksis betyr dette at du ikke trenger arv for å få polymorfisme i Python. Så lenge objektene har de riktige metodene, fungerer det.

✏️Duck typing

Her ser vi hvordan duck typing fungerer uten 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 ikke om hvilke klasser objektene tilhører. Den spør bare: «Har du en lyd()-metode?» Hvis ja, fungerer det!

Merk: Hvis vi sender inn et objekt som ikke har lyd(), får vi en AttributeError. Det er risikoen med duck typing – feil oppdages først ved kjøring.

Abstrakte klasser

Noen ganger vil vi lage en superklasse som aldri skal instansieres direkte. Den finnes bare for å definere et felles grensesnitt som subklassene implementere.

For eksempel: Det gir mening å lage en Sirkel eller Rektangel, men en generisk Form alene gir ikke mening – hva slags form er det? Vi kan bruke abstrakte klasser for å uttrykke dette.

Python har modulen abc (Abstract Base Classes) som lar oss definere abstrakte klasser og metoder.

Abstrakt klasse og abstrakt metode

- Abstrakt klasse: En klasse som ikke kan instansieres direkte. Den fungerer som en mal som subklasser må følge.
- Abstrakt metode: En metode som er deklarert i den abstrakte klassen, men som ikke har noen implementasjon. Subklasser implementere (overstyre) alle abstrakte metoder.

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

✏️Abstrakte klasser 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 poenger:
1. Form arver fra ABC (Abstract Base Class) – dette gjør den abstrakt
2. areal() og omkrets() har @abstractmethod – subklasser implementere dem
3. vis_info() er en vanlig metode som alle subklasser arver
4. Du kan ikke lage et objekt av Form direkte – du får TypeError

✏️Manglende implementasjon gir feil

La oss se hva som skjer når vi glemmer å implementere en 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 tvinger oss til å implementere alle abstrakte metoder. Dette er en kraftig sikkerhet – det garanterer at alle subklasser har den funksjonaliteten vi forventer.

Rettet 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:]}"

Nå kan vi lage et Kortbetaling-objekt uten feil.

Grensesnitt og protokoller i Python

I mange programmeringsspråk (som Java og C#) finnes det egne grensesnitt (interfaces) som definerer hvilke metoder en klasse må ha. Python har ikke et eget interface-nøkkelord, men tilbyr to alternativer:

1. Abstrakte klasser (som vi nettopp har sett) – fungerer som grensesnitt
2. Protokoller (Protocol fra typing-modulen) – en mer moderne tilnærming

En protokoll definerer et sett med metoder som et objekt må ha, uten å kreve arv. Det er en formalisering av duck typing.

✏️Protokoller 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)

Forskjell fra abstrakte klasser:
- Protokoller krever ingen arv – klassen trenger bare å ha riktige metoder
- Protokoller er primært for typesjekkere (som mypy), ikke kjøretidskontroll
- Abstrakte klasser gir feilmelding ved kjøring, protokoller ved typesjekkingen

Åpen/lukket-prinsippet

Polymorfisme og abstraksjon gjør det mulig å følge et viktig designprinsipp i programvareutvikling: åpen/lukket-prinsippet (Open/Closed Principle).

> Programvare skal være åpen for utvidelse, men lukket for endring.

Hva betyr dette i praksis? Det betyr at vi skal kunne legge til ny funksjonalitet (nye klasser) uten å endre eksisterende kode. La oss se et eksempel.

✏️Åpen/lukket-prinsippet i praksis
Dårlig design – lukket for utvidelse:

# 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 – åpen for utvidelse:

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)

Forskjellen:
- I den dårlige versjonen må vi endre prosesser_betaling() hver gang
- I den gode versjonen legger vi bare til en ny klasse – eksisterende kode røres ikke

Polymorfisme med innebygde funksjoner

Polymorfisme er ikke bare noe vi lager selv – det er innebygd i Python! Mange av Pythons innebygde funksjoner bruker polymorfisme via dunder-metoder (dobbel understrek, «double underscore»).

For eksempel bruker len() metoden __len__(), str() bruker __str__(), og + bruker __add__(). Ved å implementere disse i våre egne klasser kan vi gjøre objektene våre kompatible med Pythons innebygde funksjoner.

✏️Dunder-metoder 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__ integreres klassen vår med Pythons innebygde funksjoner. Dette er polymorfisme i praksis!

Oppsummering

I dette kapittelet har du lært:

- Polymorfisme: objekter av ulike klasser behandles likt.
- Duck typing: typen bestemmes av oppførsel, ikke klasse.
- Abstrakt klasse: kan ikke instansieres, fungerer som mal.
- Åpen/lukket-prinsippet: åpen for utvidelse, lukket for endring.
- Dunder-metoder: gir polymorfisme med innebygde funksjoner.

Noekkelbegreper


BegrepForklaring
PolymorfismeObjekter av ulike klasser behandles likt
Duck typingType bestemmes av oppførsel
Abstrakt klasseMal som ikke kan instansieres 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.