Polymorfisme, abstrakte klasser og grensesnitt.
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.
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.
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.00Nø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.
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 må 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: 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 må implementere (overstyre) alle abstrakte metoder.
I Python bruker vi dekoratoren @abstractmethod fra abc-modulen for å markere en 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 poenger:
1. Form arver fra ABC (Abstract Base Class) – dette gjør den abstrakt
2. areal() og omkrets() har @abstractmethod – subklasser må 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
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 kvitteringPython 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.
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.
# 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.
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__ 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
| Begrep | Forklaring |
|---|---|
| Polymorfisme | Objekter av ulike klasser behandles likt |
| Duck typing | Type bestemmes av oppførsel |
| Abstrakt klasse | Mal 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.