Polymorfisme, abstrakte klasser og grensesnitt.
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.
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.
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.00Nø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.
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 må 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: 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 må implementere (overstyre) alle abstrakte metodar.
I Python bruker vi dekoratoren @abstractmethod frå abc-modulen for å markere ein metode som abstrakt.
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 FormViktige poeng:
1. Form arvar frå ABC (Abstract Base Class) – dette gjer han abstrakt
2. areal() og omkrets() har @abstractmethod – subklassar må 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
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 kvitteringPython 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.
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.
# 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.
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) # FalseVed å 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
| Begrep | Forklaring |
|---|---|
| Polymorfisme | Objekt av ulike klassar vert behandla likt |
| Duck typing | Type vert bestemt av oppførsel |
| Abstrakt klasse | Mal 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.