Arv mellom klasser, super() og metodeoverstyring.
Må du skrive alt på nytt?
Tenk deg at du har laget en god Bil-klasse med massevis av nyttig funksjonalitet. Nå vil du lage en Elbil-klasse. En elbil er en bil, men med noen ekstra egenskaper som batteri og lading. Må du skrive all Bil-koden på nytt? Nei – og det er nettopp her arv kommer inn. Arv lar deg lage en ny klasse basert på en eksisterende: du arver alle attributter og metoder, og legger deretter til eller endrer det du trenger.
Arv er en av de fire grunnpilarene i OOP, og handler om gjenbruk av kode og hierarkiske relasjoner. Klassen det arves fra kalles superklassen (også foreldreklasse eller base class), og klassen som arver kalles subklassen (barneklasse eller derived class). En enkel test avgjør om arv passer: «is-a»-relasjonen. En Elbil er en Bil – da passer arv. Syntaksen i Python er rett fram:
class Dyr:
def __init__(self, navn, alder):
self.navn = navn
self.alder = alder
def beskriv(self):
print(f"{self.navn} er {self.alder} år gammel")
class Hund(Dyr):
def lag_lyd(self):
print(f"{self.navn} sier: Voff!")Hund arver __init__ og beskriv fra Dyr – vi slipper å skrive dem på nytt – og kan i tillegg ha sin egen lag_lyd.
super() og override
Når subklassen trenger flere attributter, dukker det opp en fristelse: å kopiere all oppsettskoden fra superklassens __init__. Det er nettopp det vi vil unngå. Løsningen er super(), som kaller superklassens metoder. I konstruktøren lar den oss gjenbruke foreldreklassens oppsett og bare legge til vårt eget:
class Hund(Dyr):
def __init__(self, navn, alder, rase):
super().__init__(navn, alder) # kaller Dyr sin __init__
self.rase = raseFordelene er klare: vi unngår duplisering, og hvis Dyr sin __init__ endres, fungerer Hund fortsatt. Når en subklasse definerer en metode med samme navn som i superklassen, sier vi at den overstyrer (override) metoden. Her finnes det to varianter. Du kan erstatte metoden helt, eller du kan utvide den ved å kalle super() først og deretter legge til ekstra. En Sparekonto kan for eksempel overstyre ta_ut med en strengere regel om minste saldo, men fortsatt gjenbruke den opprinnelige uttakslogikken ved å avslutte med return super().ta_ut(belop). Slik bygger spesialiseringen videre på det generelle uten å miste det.
isinstance() og hierarkier
Noen ganger må du sjekke hvilken type et objekt har. Til det bruker vi isinstance(objekt, Klasse), som returnerer True eller False. Det smarte er at den følger arvelinjen: er hund en Hund, er isinstance(hund, Hund) sant, men også isinstance(hund, Dyr), fordi en Hund er et Dyr. Dette er nyttig når en funksjon skal behandle ulike typer forskjellig. Skriver du en fôr_dyr-funksjon, må du sjekke de mest spesifikke typene først – Hund og Katt før Dyr – nettopp fordi en Hund også teller som et Dyr. Sjekker du Dyr først, fanger den alt.
Arv lar oss bygge hierarkier fra det generelle til det spesifikke. Et Dyr kan ha en subklasse Pattedyr, som igjen har subklassen Hund. Da arver Hund nedover hele kjeden: spis fra Dyr, die fra Pattedyr og sin egen bjeff. «is-a»-relasjonen gjelder hele veien: en Hund er et Pattedyr, et Pattedyr er et Dyr, altså er en Hund et Dyr. Å bevege seg nedover i hierarkiet kalles spesialisering, mens veien oppover er generalisering. isinstance bekrefter alt: en Hund er sann mot Hund, Pattedyr og Dyr på samme tid.
Er-en eller har-en?
Arv er kraftig, men det kan misbrukes. Den viktigste avgjørelsen er om relasjonen er «is-a» eller «has-a». Bruk arv når det er en klar er-en-relasjon, når subklassen er en spesialisering, når du vil gjenbruke mye felles kode, og når endringer i superklassen skal slå gjennom i subklassene. Unngå arv når relasjonen egentlig er har-en, når du bare vil gjenbruke litt kode, eller når hierarkiet blir for dypt – mer enn tre-fire nivåer blir fort uoversiktlig.
Det klassiske eksempelet er en bil og en motor. Det ville være feil å la Bil arve fra Motor, for en bil er ikke en motor. En bil har en motor. Det modellerer vi med komposisjon, ved at bilen lager et Motor-objekt inni seg:
class Bil:
def __init__(self, merke):
self.merke = merke
self.motor = Motor(150) # Bil HAR en motorDet samme gjelder andre par: et hus har en dør, en laptop har en skjerm, en bil har et gir – alle er har-en, og ingen bør bruke arv. Derimot er «en elev er en person» en ekte er-en-relasjon. Et nyttig motto når du er i tvil: «favor composition over inheritance» – foretrekk komposisjon over arv.
Oppsummering
Vi startet med spørsmålet om du må skrive alt på nytt for en elbil, og svaret var et rungende nei. Arv lar en subklasse overta attributter og metoder fra en superklasse, med syntaksen class Subklasse(Superklasse):. Med super() kaller du superklassens metoder, spesielt i konstruktøren, slik at oppsettskode gjenbrukes. Override lar deg overstyre en arvet metode – enten erstatte den helt eller utvide den ved å kalle super() først.
isinstance(objekt, Klasse) sjekker om et objekt er av en klasse eller arver fra den, og følger hele arvelinjen. Arv lar oss bygge hierarkier fra generelt til spesifikt, der spesialisering går nedover og generalisering oppover. Den viktigste designvurderingen er om noe er en «is-a» eller en «has-a»: arv passer for er-en (en elev er en person), mens komposisjon passer for har-en (en bil har en motor). Når du er usikker: foretrekk komposisjon. I neste kapittel ser vi på polymorfisme og abstraksjon – hvordan ulike objekter kan brukes på samme måte.
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.