5.1 Klasser, `__init__` og `__call__`
Definere en klasse, lagre attributter i `__init__` og gjøre instansen kallbar med `__call__` — funksjonsklassen som er selve arbeidshesten.
- Klasser med spesialmetoder står i 8 av 8 (100 %) av de avsluttende eksamenssettene 2018–2025. En spesialmetode er en metode med to understreker på hver side av navnet, som Python kaller automatisk for deg — du ser den aldri i et kall.
- Oppgavetypen kalles sjanger G i denne boka: «skriv en klasse med spesialmetoder». Den er fritekst-koding — du skriver koden selv i et tekstfelt, uten hjelpemidler og uten å kunne kjøre den. Vekten er 8–12 poeng av 75 på den avsluttende eksamenen (den fire timer lange desembereksamenen, som teller 75 % av karakteren).
- Dette kapitlet dekker de to spesialmetodene som bærer alt annet: konstruktøren __init__ og den som gjør instansen kallbar, __call__. Til sammen gir de deg funksjonsklassen, som er selve arbeidshesten i faget.
- Sjanger O («finn feilen i programmet») bruker fast den glemte self-en som plantet feil. Den er verdt to gratispoeng når du vet hvor du skal se.
- Prioritet: dette må sitte. Klasser er ett av de fem temaene som er med i hvert eneste sett. Til trøst: sensor gir god uttelling for riktig klasseoppsett selv om en detalj i utregningen er feil.
1. En funksjon kan ha parametre med default-verdier. Skriver du ikke argumentet, brukes verdien i parameterlisten:
from math import exp, pi, sqrt
def gauss(x, m=0, s=1):
return 1/(sqrt(2*pi)*s) * exp(-0.5*((x - m)/s)**2)
print(gauss(0))
print(gauss(1, 0, 2))Utskrift:
0.3989422804014327
0.176032663382149762. En funksjon kan sendes som argument til en annen funksjon. Da sender du navnet UTEN parentes — tabell(kvadrat, ...), ikke tabell(kvadrat(), ...):
def tabell(f, a, b, n):
h = (b - a)/n
for i in range(n+1):
x = a + i*h
print(x, f(x))
def kvadrat(x):
return x*x
tabell(kvadrat, 0, 1, 2)Utskrift:
0.0 0.0
0.5 0.25
1.0 1.03. Innrykk avgjør hva som hører sammen. Alt som er rykket inn fire mellomrom under en def-linje, er kroppen til den funksjonen. Samme regel gjelder inne i en klasse, som du skal se nå.
Løkke 1 — Klassen som en boks med parametre (~15 min)
Problemet: parametrene som må slepes med overalt
Tenk på normalfordelingskurven, den som beskriver måleusikkerhet i et laboratorium:
Kurven har to parametre: sentrum og bredden . Skriver du den som en vanlig funksjon, må du ha tre argumenter: gauss(x, m, s).
Det er greit så lenge du bare regner ut enkeltverdier. Problemet oppstår i det du vil gi kurven videre til noe annet. En numerisk derivator, en plottefunksjon og en nullpunktsløser har alle samme forventning: de vil ha en funksjon som tar ett argument, f(x). Din funksjon tar tre.
Det er nettopp dette en klasse løser. Du pakker og inn i objektet én gang, og etterpå oppfører objektet seg som en funksjon av alene.
En klasse er en oppskrift på en type objekter. Du skriver class etterfulgt av navnet og et kolon, og alt som er rykket inn under, hører til klassen.
Navnet skrives med stor forbokstav — Gauss, Vec2D, Diff. Det er ikke et krav fra Python, men det er konvensjonen sensor forventer, og det gjør koden lesbar: stor forbokstav betyr «klasse», liten betyr «variabel eller funksjon».
class Gauss:
passKlassen i seg selv er bare oppskriften. Den gjør ingenting før du lager et objekt av den.
Et konkret objekt laget etter oppskriften klassen gir. Klassen Gauss er én ting; kurven med og er en instans av den.
Du lager en instans ved å skrive klassenavnet med parentes: g = Gauss(0, 1). Nå er g en instans, og du kan lage så mange du vil — hver med sine egne parameterverdier.
Ordet «objekt» og ordet «instans» brukes om hverandre i faget. Bruk gjerne «instans» når du vil understreke at objektet er laget fra en bestemt klasse.
Metoden Python kaller automatisk når du lager en instans. Den kalles aldri ved navn i vanlig kode: skriver du Gauss(0, 1), kjører Python __init__ med m = 0 og s = 1 for deg.
Jobben til __init__ er å ta imot argumentene og lagre dem i instansen, ett self.navn = navn per parameter. Ingenting annet, som regel.
class Gauss:
def __init__(self, m, s):
self.m = m
self.s = s__init__ skal ikke returnere noe. Skriver du return med en verdi der, får du en feilmelding — konstruktøren lager ikke verdien, den fyller instansen.
Det første parameteret i alle metoder. self er selve instansen metoden kalles på — det er Python som sender den inn, automatisk, som første argument.
Derfor er det to ting du må vane deg til, og de er hver sin halvdel av samme regel:
- I signaturen skriver du alltid self først: def __call__(self, x):.
- I kallet skriver du den aldri: g(2), ikke g(g, 2).
Inne i en metode er self din eneste vei til instansens egne data. self.m er instansens ; bar m er noe helt annet, nemlig en lokal variabel som ikke finnes med mindre du har laget den.
En variabel som hører til instansen og lever så lenge instansen lever. Linjen self.m = m oppretter attributtet m på instansen og gir det verdien argumentet hadde.
Attributter er nettopp poenget med en klasse: de er parametrene som slipper å bli sendt inn på nytt hver gang. Etter g = Gauss(0, 1) husker g for alltid at self.m er 0.
Utenfra leser du et attributt med punktum: g.m. Inne i en metode skriver du self.m. Det er samme attributt, sett fra to steder.
Hver instans har sine egne attributter. Endrer du g.m, skjer det ingenting med en annen instans h.
Skriv en klasse Gauss som representerer normalfordelingskurven . Sentrum m og bredden s skal gis når objektet lages, og en metode verdi skal regne ut funksjonsverdien i et punkt. Lag to instanser og skriv ut noen verdier.
from math import exp, pi, sqrt
class Gauss:
def __init__(self, m, s):
self.m = m
self.s = s
def verdi(self, x):
return 1/(sqrt(2*pi)*self.s) * exp(-0.5*((x - self.m)/self.s)**2)
g = Gauss(0, 1)
h = Gauss(2, 0.5)
print(g.m, g.s)
print(g.verdi(0))
print(h.verdi(2))
print(g.verdi(2))Utskrift:
0 1
0.3989422804014327
0.7978845608028654
0.05399096651318806Legg merke til tre ting i utskriften. Først: g.m og g.s er de verdiene vi sendte inn — de bor i instansen nå. Dernest: g.verdi(0) er toppunktet på standardkurven, . Og til sist: h.verdi(2) er toppunktet på den smale kurven, som er høyere fordi arealet under kurven alltid er 1.
Den siste linjen er den viktige: g.verdi(2) gir en liten verdi, fordi ligger to standardavvik ute på g-kurven. Samme metode, samme argument, forskjellig svar — fordi de to instansene har forskjellige attributter. Det er hele poenget med en klasse.
(Innstegsoppgave, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) To værstasjoner er laget som instanser av samme klasse. Skriv utskriften nøyaktig, med mellomrom og linjeskift slik terminalen viser den.
class Maalestasjon:
def __init__(self, navn, hoyde):
self.navn = navn
self.hoyde = hoyde
def beskriv(self):
return self.navn + ' ligger ' + str(self.hoyde) + ' moh.'
a = Maalestasjon('Blindern', 94)
b = Maalestasjon('Fanaraaken', 2062)
print(a.beskriv())
print(b.hoyde - a.hoyde)
print(a.navn, b.navn)Dette er fagets mest plantede feil, og den har to former. Den heter #3 — den glemte self-en i bokas feilkatalog (feilene har numre så du kan slå dem opp i studieguiden).
Form 1 — glemt i signaturen. Du skriver def __call__(x): i stedet for def __call__(self, x):. Python sender likevel instansen som første argument, så x blir instansen, og det egentlige argumentet har ingen plass å være. Siste linje i feilmeldingen blir:
TypeError: __call__() takes 1 positional argument but 2 were givenForm 2 — glemt i attributt-tilgangen. Du skriver return a*t når du mente return self.a*t. Nå leter Python etter en vanlig variabel a, finner ingen, og sier:
NameError: name 'a' is not definedBegge feilene er trivielle å rette og dyre å overse. Vanen som redder deg på eksamen: skriv self. før du skriver navnet, hver gang du er inne i en metode og skal bruke noe instansen eier.
Løkke 2 — __call__: instansen som oppfører seg som en funksjon (~20 min)
Spesialmetoden som gjør at du kan skrive parentes rett etter objektet. Har klassen en __call__, er g(2) lovlig, og det betyr nøyaktig det samme som g.__call__(2).
Da er g kallbar: den kan brukes overalt der Python venter en funksjon av ett argument. Det er dette som løser problemet vi startet med — parametrene bor i instansen, og utsiden ser bare f(x).
class Gauss:
def __init__(self, m, s):
self.m = m
self.s = s
def __call__(self, x):
return exp(-0.5*((x - self.m)/self.s)**2)Uten __call__ er g(2) en feil: «'Gauss' object is not callable». Med den kan du skrive g(2), sende g til en derivator, eller plotte den — helt som en vanlig funksjon.
Dette er kodemønsteret Del 5 handler om, og det du skal kunne skrive fra bunnen uten å tenke. Det består av tre linjer struktur og én linje matematikk:
class Navn:
def __init__(self, p1, p2):
self.p1 = p1
self.p2 = p2
def __call__(self, x):
return ... # formelen, med self.p1 og self.p2Arbeidsdelingen er fast, og den er hele hemmeligheten:
1. __init__ tar imot alt som er konstant for kurven — parametre, konstanter, en annen funksjon, et steg. Ett self. per argument.
2. __call__ tar imot det som varierer — nesten alltid den ene variabelen (eller ) — og returnerer funksjonsverdien.
Går du feil her, går resten av oppgaven feil, så still deg selv spørsmålet før du skriver: hva er fast for kurven, og hva varierer? Det faste hører i __init__, det varierende i __call__.
Det som gjør spesialmetoder forvirrende er ikke syntaksen, men at ingen av dem kalles ved navn. La oss legge inn en utskrift i hver metode, slik at rekkefølgen av kall blir synlig i terminalen.
from math import exp
class Gauss:
def __init__(self, m, s):
print('__init__ kjorer, m =', m, 's =', s)
self.m = m
self.s = s
def __call__(self, x):
print('__call__ kjorer, x =', x)
return exp(-0.5*((x - self.m)/self.s)**2)
print('for instansiering')
g = Gauss(0, 2)
print('etter instansiering')
y = g(2)
print('verdien er', y)Utskrift:
for instansiering
__init__ kjorer, m = 0 s = 2
etter instansiering
__call__ kjorer, x = 2
verdien er 0.6065306597126334Les utskriften ovenfra og ned, for den forteller hele historien:
- __init__ kjører én gang, i det øyeblikket Gauss(0, 2) utføres. Argumentene 0 og 2 går til __init__.
- Mellom de to metodene skjer det ingenting. Instansen bare ligger der med attributtene sine.
- __call__ kjører først når du skriver g(2). Argumentet 2 går til __call__, ikke til __init__ — det er to helt forskjellige parameterlister.
De to print-linjene inne i metodene står her bare for å avsløre rekkefølgen. Fjern dem i en eksamensbesvarelse: sensor vil ha kort, ren kode uten ekstra utskrift.
(Midtveisnivå, sjanger A — kodesporing.) Programmet under er kjørt ferdig, slik at r finnes.
class Rampe:
def __init__(self, a):
self.a = a
def __call__(self, t):
return self.a*t
r = Rampe(3)For hvert av de fire uttrykkene under: si hvilken metode Python kjører (eller at ingen metode kjøres), og hva uttrykket har som verdi.
a) Rampe(5)
b) r(4)
c) r.a
d) r.__call__(4)
Skriv en funksjon maksverdi(f, a, b, n) som finner den største verdien av f i jevnt fordelte punkter på . Bruk den på to Gauss-kurver — uten å endre en linje i maksverdi.
from math import exp, pi, sqrt
class Gauss:
def __init__(self, m=0, s=1):
self.m = m
self.s = s
def __call__(self, x):
return 1/(sqrt(2*pi)*self.s) * exp(-0.5*((x - self.m)/self.s)**2)
def maksverdi(f, a, b, n):
stor = f(a)
for i in range(1, n+1):
x = a + i*(b - a)/n
if f(x) > stor:
stor = f(x)
return stor
standard = Gauss()
smal = Gauss(2, 0.5)
print(maksverdi(standard, -4, 4, 80))
print(maksverdi(smal, -4, 4, 80))Utskrift:
0.3989422804014327
0.7978845608028654Se på signaturen til maksverdi: den vet ingenting om klasser. Den kaller bare f(x). Likevel virker den, fordi begge instansene er kallbare.
Dette er grunnen til at funksjonsklassen er så nyttig i faget. Alt maskineriet du møter senere — numerisk derivasjon, integrasjon, ODE-løsere, plotting — ber om «en funksjon av ett argument». En instans med __call__ er en slik funksjon, men med parametre på innsiden.
Legg også merke til Gauss() uten argumenter. Det virker fordi parameterlisten har default-verdier, m=0 og s=1.
Utskriftsdetalj (feil #16 — utskriftsdetaljer oversett): den øverste verdien er toppen av standardkurven, . Den skrives med alle desimalene Python har, fordi vi ikke ba om formatering.
En parameter kan ha en standardverdi som brukes hvis den som lager instansen ikke oppgir noe. Formen er def __init__(self, f, h=1e-5):.
Da virker begge disse:
- Diff(sin) — h blir 1e-5.
- Diff(sin, 1e-3) — h blir 1e-3.
Regelen er at parametre med default-verdi må stå sist i parameterlisten. def __init__(self, h=1e-5, f): er en syntaksfeil.
Eksamensoppgaver ber ofte eksplisitt om en default-verdi («med et steg h som har standardverdi »). Da er det ett poeng å skrive den inn i signaturen, ikke inne i metodekroppen.
Løkke 3 — Klassen som pakker inn en annen funksjon (~20 min)
Attributtet trenger ikke være et tall
Så langt har attributtene våre vært tall. Men self.f = f er like lovlig: et attributt kan være en funksjon. Da har du en klasse som pakker inn en annen funksjon og gjør noe med den.
Det er nettopp det mønsteret eksamen bruker til numerisk derivasjon. Du skal lage et objekt som er «den deriverte av f» — og som selv er en vanlig funksjon du kan kalle, tabulere eller plotte.
Den sentrerte deriverte er
med et lite steg . Nå skal formelen inn i en __call__.
Skriv en klasse Diff som tar en funksjon f og et steg h med standardverdi i konstruktøren, og der kallet d(x) returnerer den sentrerte deriverte . Vis at Diff(sin) oppfører seg som cosinus.
from math import sin, cos
class Diff:
def __init__(self, f, h=1e-5):
self.f = f
self.h = h
def __call__(self, x):
f, h = self.f, self.h
return (f(x + h) - f(x - h))/(2*h)
dsin = Diff(sin)
print(dsin(0))
print(cos(0))
print(dsin(1))
print(cos(1))
grov = Diff(sin, 0.1)
print(grov(1))Utskrift:
0.9999999999833332
1.0
0.5403023058569989
0.5403023058681398
0.53940225216976Kodemønsteret er malen fra i stad, med én ny vending: self.f er en funksjon, så f(x + h) inne i __call__ kaller den funksjonen brukeren sendte inn.
Linjen f, h = self.f, self.h er en vanlig og god vane i dette faget. Den koster én linje og gjør formelen etterpå lesbar — sammenlign med (self.f(x + self.h) - self.f(x - self.h))/(2*self.h), som er samme sak, men vondt å lese og lett å skrive feil.
Se på tallene: med treffer den numeriske deriverte cosinus til rundt ti desimaler. Med (den siste instansen) sitter bare de to første — avviket er rundt . Det er hele avveiningen i numerisk derivasjon i et nøtteskall.
Uttelling på eksamen: de tre poengbærende elementene her er (i) at __init__ lagrer både f og h som attributter, (ii) at default-verdien står i signaturen, og (iii) at __call__ bruker den sentrerte formelen med 2*h i nevneren. Bytter du fortegn på et ledd, mister du et halvt poeng — ikke oppgaven.
(Midtveisnivå, sjanger A — kodesporing.) Hva skriver programmet ut? Skriv utskriften nøyaktig, med mellomrom og linjeskift slik terminalen viser den.
class Potens:
def __init__(self, n=2):
self.n = n
def __call__(self, x):
return x**self.n
p = Potens()
q = Potens(3)
print(p(4), q(2))
print(p(2) + q(1))
print(p.n, q.n)der er romtemperaturen, starttemperaturen og en avkjølingskonstant.
a) Skriv en klasse Avkjoling der de tre parametrene gis når objektet lages, og der kallet T(t) gir temperaturen ved tiden t.
b) Lag en instans med , og , og skriv ut temperaturen ved , og .
De to metodene har hver sin parameterliste, og de har ingenting med hverandre å gjøre. Det er lett å tro at __call__ også må ta imot parametrene.
Feil variant, der x er dyttet inn i konstruktøren:
class Gauss:
def __init__(self, m, s, x):
self.m = m
self.s = s
self.x = x
def __call__(self):
return exp(-0.5*((self.x - self.m)/self.s)**2)Dette kjører, men det ødelegger hele poenget: nå må du lage en ny instans for hver -verdi, og instansen kan ikke sendes til en derivator eller en plottefunksjon, for den er ikke en funksjon av lenger.
Blokken over er merket text og ikke python fordi den er en visning av noe du ikke skal skrive. Testen din er alltid den samme: det som varierer, skal inn gjennom __call__.
Programmet under skal regne ut en Gauss-verdi, men det virker ikke. Finn alle feilene, forklar hva hver av dem gjør, og skriv programmet riktig.
class Gauss:
def __init__(m, s):
m = m
s = s
def __call__(x):
return exp(-0.5*((x - self.m)/self.s)**2)
g = Gauss(0, 1)
print(g(1))der er bærekapasiteten, startbestanden og vekstraten.
a) Skriv en klasse Logistisk der K, N0 og r gis i konstruktøren, og der kallet gir bestanden ved tiden t. Vekstraten skal ha standardverdien .
b) Skriv ut bestanden ved for og , med to desimaler.
c) Bruk klassen Diff fra eksempel 4 til å regne ut vekstfarten — uten å endre en linje i Diff.
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
En funksjon som er definert inne i en klassekropp. Metoder kalles på en instans med punktum: g.verdi(2).
Forskjellen fra en vanlig funksjon er self: metoden har tilgang til instansens attributter, og trenger derfor færre argumenter.
verdi, __init__ og __call__ er alle metoder. De to siste er i tillegg spesialmetoder, og det er den eneste forskjellen på dem.
En metode med to understreker på hver side av navnet, som Python kaller automatisk i bestemte situasjoner. __init__ kalles av Navn(...), __call__ av obj(...).
I dagligtale kalles de «dunder-metoder», etter «double underscore». Du gjenkjenner dem på formen __navn__.
Det som gjør dem forvirrende for nybegynneren, er nettopp at de ikke kalles ved navn: du skriver g(2) og __call__ kjører. Regel når du sporer kode: finn ut hva uttrykket ser ut som, og spør hvilken spesialmetode den formen utløser.
IN1900 bruker fem av dem: __init__, __call__, __str__, __add__ og __mul__. De tre siste kommer i kap. 5.2.
Handlingen å lage en instans fra en klasse. Du skriver klassenavnet med parentes, og argumentene i parentesen går til __init__.
To ting skjer, i denne rekkefølgen: Python lager et tomt objekt, og deretter kjører __init__ på det og fyller det med attributter.
Verdien av uttrykket Navn(...) er instansen selv. Derfor kan du både lagre den i en variabel, sende den rett inn i en funksjon, eller putte den i en liste.
En klasse som representerer én matematisk funksjon med parametre: konstruktøren tar parametrene, og __call__ regner ut funksjonsverdien.
Dette er bokas viktigste klassetype, og eksamensoppgavene bruker den om alt fra hyperbolske funksjoner til polynomer og deriverte.
Kjennetegnet er at instansen kan brukes overalt der Python venter f(x). Du kan tabulere den, derivere den numerisk, plotte den eller sende den til en ODE-løser.
Et objekt du kan skrive parentes etter. Vanlige funksjoner er kallbare, og en instans er kallbar hvis — og bare hvis — klassen har en __call__.
Mangler den, sier Python fra med siste linje: TypeError: 'Rampe' object is not callable. Det er en av de mest presise feilmeldingene i Python, og den betyr nesten alltid at du glemte __call__ eller stavet den feil.
Vær nøye med navnet: __call__ med to understreker på hver side. _call_ eller __call gjør ingenting — det blir bare en vanlig metode med et rart navn.
Det samme attributtet, sett fra to steder. Utenfor klassen bruker du instansnavnet: g.m. Inne i en metode bruker du self: self.m.
Du kan også endre et attributt utenfra, med g.m = 5. Det er lovlig Python, og det gjør at neste kall til g(x) regner med den nye verdien. På eksamen dukker dette opp i sporingsoppgaver: en instans lages, et attributt endres, og så kalles instansen.
Attributt-tilgang kjører ingen metode. Det er bare et oppslag i objektet.
En variabel som opprettes uten self. inne i en metode. Den finnes bare mens metoden kjører, og forsvinner når metoden er ferdig.
Det er dette som gjør m = m i en konstruktør til en stille katastrofe: linjen er lovlig, programmet krasjer ikke der, men instansen får aldri noe attributt. Feilen viser seg først senere, som AttributeError eller NameError et helt annet sted.
Lokale variabler er nyttige når du vil kort ned en formel, som i f, h = self.f, self.h. Skillet du må ha automatisk: med self. = lagres i instansen, uten self. = forsvinner.
Konstruktørens jobb er å fylle instansen, ikke å levere en verdi. Den har ingen return-setning med verdi.
Prøver du likevel return self.m + self.s i __init__, får du TypeError: __init__() should return None. Verdien av Navn(...) er nemlig alltid instansen, og den bestemmer du ikke over.
Konsekvensen i praksis: g = Gauss(0, 1) gir deg objektet, aldri et tall. Vil du ha et tall ut, må du kalle instansen — g(2) — eller lese et attributt.
Samme tegn, to helt forskjellige jobber, og det avhenger av hva som står rett foran parentesen.
- Parentes etter et klassenavn — Gauss(0, 1) — lager en instans og kjører __init__.
- Parentes etter en instans — g(2) — kjører __call__.
Dette er den enkle regelen som løser de fleste sporingsoppgavene om klasser. Se på navnet foran parentesen: stor forbokstav betyr som regel «her lages det et objekt», liten forbokstav betyr «her brukes et objekt».
Hver instans har sitt eget sett attributter. g = Gauss(0, 1) og h = Gauss(2, 0.5) deler ingenting: g.s er 1 uansett hva som skjer med h.s.
Det er derfor det er billig å lage mange instanser. Én kurve per parametersett, og alle kan brukes side om side i samme program — for eksempel plottes i samme figur.
Sporingsfellen som følger av dette: når to instanser er laget fra samme klasse, må du holde to kolonner i sporingstavlen din, én per instans. Blander du dem, får du feil svar selv om du forstår koden.
Inne i __init__ finnes navnet i to versjoner: a er argumentet som kom inn, self.a er attributtet du lagrer det i. Linjen self.a = a kopierer det ene til det andre.
Etter at __init__ er ferdig, finnes bare self.a. Derfor er return a*t i en annen metode en NameError: argumentet er borte, bare attributtet lever.
At de to har samme navn, er en ren vane — self.a = verdi ville fungert like godt. Men vanen er sensors vane også, så behold den.
Den ene avgjørelsen som bestemmer om en G-oppgave blir riktig: hvilke størrelser hører hvor.
- Det som er fast for objektet — parametre, konstanter, en innpakket funksjon, et steg — settes i __init__.
- Det som varierer fra kall til kall — nesten alltid den ene variabelen eller — er parameter i __call__.
Testen er praktisk: hvis du må lage en ny instans for hver -verdi, har du plassert noe galt.
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.
Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.