3.3 DRILL — Rekke-implementering med testfunksjon
Full drill på F-sjangeren: fra rekkeformel til kompakt funksjon + testfunksjon, med roterende innpakning.
Dette er drillkapitlet for sjanger F — «skriv en funksjon som summerer en rekke, og skriv en testfunksjon til den». Sjangeren står i hvert eneste av de åtte gjennomgåtte avsluttende settene (100 prosent) og teller 4–6 poeng, delt i to: del-a er summen, del-b er testfunksjonen. Testfunksjonen er et selvstendig delkrav i 7 av 8 sett (88 prosent).
Prioriteten er den høyeste boka har: dette må sitte.
Grunnen til at et helt kapittel går til drill, er at oppgaven er nesten uforandret fra år til år. Bare innpakningen roterer: eksponentialfunksjonen, logaritmen, cosinus, sinus og cosinus hyperbolicus, invers tangens hyperbolicus, arcus sinus, Chebyshev- og Legendre-polynomer. Formelen ser ny ut, og løkka er den samme.
Du besvarer alt som fritekst i Inspera, UiOs digitale eksamenssystem, uten hjelpemidler og uten å kunne kjøre koden. Poenget med dette kapitlet er derfor ikke å lese oppskriften, men å få den ned i fingrene: skriv koden på papir eller i en editor du ikke trykker «kjør» i.
Kapitlet har tre deler: én løsningsoppskrift på sju steg, ett gjennomarbeidet eksamenscase med sensorkommentar ved hvert steg, og tolv oppgaver på eksamensnivå med roterende innpakning.
Om delpoeng: sensor gir uttelling for hvert steg i oppskriften for seg. Riktig akkumulator, riktig løkke og riktig return belønnes selv om ledd-uttrykket har en indeksfeil. Å få alle detaljer 100 prosent riktig på en hjelpemiddelfri eksamen er nesten umulig — riktig struktur er det som teller.
Forkunnskaper — sist du var her
Drillen forutsetter kap. 3.1 (testfunksjoner) og kap. 3.2 (rekkemønsteret). Her er de tre grepene du kommer til å bruke i hver eneste oppgave, oppfrisket med kjørt utskrift.
1. Summeringsmønsteret. Nullstill, løkk, legg til, returnér — i den rekkefølgen.
from math import factorial
def exp_rekke(x, N):
s = 0
for k in range(N+1):
s += x**k / factorial(k)
return s
print(exp_rekke(1.0, 10))Utskrift:
2.71828180114638452. Testfunksjonen. Ingen argumenter, ingen utskrift, alltid toleranse.
from math import factorial, exp
def exp_rekke(x, N):
s = 0
for k in range(N+1):
s += x**k / factorial(k)
return s
def test_exp_rekke():
x = 0.5
expected = exp(x)
computed = exp_rekke(x, 15)
tol = 1e-10
msg = f'ventet {expected}, fikk {computed}'
assert abs(expected - computed) < tol, msg
test_exp_rekke()
print('Testen gikk gjennom')Utskrift:
Testen gikk gjennom3. Rekurrensen med to lagervariabler. Regn den nye verdien først, flytt lagrene etterpå.
def chebyshev(n, x):
if n == 0:
return 1.0
T_forrige = 1.0
T_denne = x
for k in range(1, n):
T_neste = 2*x*T_denne - T_forrige
T_forrige = T_denne
T_denne = T_neste
return T_denne
print(chebyshev(3, 0.5), chebyshev(4, 0.5))Utskrift:
-1.0 -0.5Er noe av dette uklart, les de to forrige kapitlene først — drillen forklarer ikke konstruksjonene på nytt.
Les oppgaveteksten, og gjør så dette, i rekkefølge. Hvert steg svarer til ett spørsmål du besvarer ved å se på formelen som står i oppgaven.
1. Skriv ned ledd-uttrykket. Alt til høyre for summetegnet, oversatt tegn for tegn til Python. Ikke forenkle. x**k / factorial(k), (-1)**k * x**(2*k) / factorial(2*k).
2. Les startverdien under summetegnet. Er den 0, er løkka range(...); er den 1, er den range(1, ...).
3. Les sluttverdien over summetegnet. «Til og med » gir range(N+1). «De første leddene» gir range(N). Er teksten tvetydig, velg og skriv én kommentarlinje.
4. Skriv de fire linjene: s = 0, løkka, s += ledd, return s.
5. Skriv testfunksjonen: ingen argumenter, expected fra en uavhengig kilde, computed fra funksjonen din, tol valgt etter metoden, og assert abs(expected - computed) < tol, msg.
6. Sjekk indekseringen på oddetalls- og partallsledd: står 2*n+1 i eksponenten, skal det stå i nevneren også.
7. Er rekka rekurrent? Da bytter du mal: basistilfelle med if, to lagervariabler, hjelpevariabel for den nye verdien, løkke fra 1 til .
Skjelettet er alltid dette, og det er verdt å skrive ned først, før du fyller inn ledd-uttrykket:
def rekke(x, N):
s = 0
for k in range(N+1):
s += 0.0
return s
print(rekke(2.0, 5))Utskrift:
0.0Skjelettet gir uttelling i seg selv. Fyller du inn feil ledd, mister du ett eller to poeng — glemmer du skjelettet, mister du oppgaven.
range. «Til og med » betyr range(N+1). Dette er den hyppigste feilen i hele emnet, og i denne sjangeren gir den systematisk ett manglende ledd — nok til at en testfunksjon med stram toleranse feiler.Akkumulator eller return på feil sted. s = 0 inne i løkka nullstiller summen hver runde; return s inne i løkka avbryter etter første ledd. Begge gir ett ledd i stedet for en sum, og begge er lette å se når du leser koden med innrykkene i tankene.
Ulik indeksering i teller og nevner. x**(2*n+1) / factorial(n) er en annen funksjon enn den oppgaven ba om. Kontrollen tar to sekunder: står samme uttrykk begge steder?
#11 — likhetstegn på flyttall i testen. assert computed == expected feiler på helt riktig kode. Formen er alltid assert abs(expected - computed) < tol, msg.
Og én til som er verdt et helt poeng: fasiten som gjentar koden. Setter du expected til samme uttrykk som funksjonen bruker, tester du ingenting. Hent fasiten fra math, fra en kjent eksakt formel, eller fra håndregning.
Rekursjon er utenfor pensum i IN1900. Rekurrente rekker frister til å la en funksjon kalle seg selv; det er utenfor pensum, markeres uønsket i de offisielle løsningsforslagene, og er dessuten svært tregt. Løkka er den forventede løsningen — alltid.
Det gjennomarbeidede caset (~20 min)
Under står en komplett sjanger-F-oppgave av samme type og vekt som på et virkelig sett, med sensorkommentar ved hvert steg. Les den som en modell: det er nøyaktig denne formen svarene dine skal ha.
Oppgaven (6 poeng). Når en lysstråle går fra luft til vann, gir brytningsloven at utgangsvinkelen er , der er sinus til innfallsvinkelen delt på brytningsindeksen. En innebygd arcsin er ikke alltid tilgjengelig i innvevd programvare, og da brukes rekka
a) (4 p) Skriv arcsin_rekke(x, N) som summerer leddene fra til og med .
b) (2 p) Skriv en testfunksjon som sammenligner med math.asin for .
Del (a), 4 poeng: arcsin_rekke(x, N) skal summere leddene til og med .
Gå gjennom oppskriften. Ledd-uttrykket har fakultet i både teller og nevner, en firerpotens og en oddetallspotens av . Startverdien er 0, sluttverdien er og «til og med», så løkka er range(N+1).
Skriv ledd-uttrykket som det står. (2n)! blir factorial(2*n), blir 4**n, blir factorial(n)**2, og blir (2*n+1).
from math import factorial
def arcsin_rekke(x, N):
s = 0
for n in range(N+1):
s += factorial(2*n) * x**(2*n+1) / (4**n * factorial(n)**2 * (2*n+1))
return s
for N in [0, 2, 5, 20]:
print(N, arcsin_rekke(0.4, N))Utskrift:
0 0.4
2 0.41143466666666667
5 0.4115167123246176
20 0.41151684606748806Sensorkommentar, steg for steg:
- s = 0 før løkka — 0,5 p. En ren rutinelinje, men den gis poeng, og den kan skrives før du har forstått formelen.
- for n in range(N+1): — 1 p. Dette er stedet av-én-feilen slår til. range(N) gir samme kode med ett ledd mindre.
- Ledd-uttrykket — 2 p. Delt i to: 1 poeng for oddetallspotensen x**(2*n+1), 1 poeng for nevneren med alle tre faktorene. Bommer du på én faktor i nevneren, får du 1 av 2.
- return s utenfor løkka — 0,5 p. Står return inne i løkka, mister du dette og strukturpoenget over.
Hva som er godt nok. Skriver du nevneren som 4**n * factorial(n)*factorial(n) * (2*n+1), er det like riktig. Deler du ledd-uttrykket i to linjer med en hjelpevariabel ledd, er det også like riktig — og litt lettere å lese:
from math import factorial
def arcsin_rekke(x, N):
s = 0
for n in range(N+1):
teller = factorial(2*n) * x**(2*n+1)
nevner = 4**n * factorial(n)**2 * (2*n+1)
s += teller / nevner
return s
print(arcsin_rekke(0.4, 20))Utskrift:
0.41151684606748806Kontrollen du kan gjøre uten datamaskin: med er leddet selv, siden , og . Utskriften bekrefter det — 0,4 på første linje. Gir din funksjon noe annet, er indekseringen gal, og det oppdager du på ti sekunder.
Del (b), 2 poeng: skriv en testfunksjon mot math.asin for . Hvor mange ledd må du ta med for at tol = 1e-10 skal holde?
Fasiten hentes fra math.asin, som regner ut arcus sinus på en helt annen måte enn rekka — det er nettopp det som gjør den til en uavhengig kilde.
from math import factorial, asin
def arcsin_rekke(x, N):
s = 0
for n in range(N+1):
s += factorial(2*n) * x**(2*n+1) / (4**n * factorial(n)**2 * (2*n+1))
return s
def test_arcsin_rekke():
x = 0.4
expected = asin(x)
computed = arcsin_rekke(x, 20)
tol = 1e-10
msg = f'ventet {expected}, fikk {computed}'
assert abs(expected - computed) < tol, msg
test_arcsin_rekke()
print('Testen gikk gjennom')
print(asin(0.4))Utskrift:
Testen gikk gjennom
0.41151684606748806Fra tabellen i del (a) ser du hvor mange ledd som trengs: med tre ledd () stemmer tre desimaler, med seks ledd stemmer seks, og med 21 ledd er alle sifrene like. Toleransen 1e-10 krever altså et sted mellom ti og tjue ledd, og er et trygt valg.
Sensorkommentar del (b):
- Testfunksjon uten argumenter, med navn test_ pluss det som testes — 0,5 p.
- expected fra math.asin, altså en uavhengig kilde — 0,5 p.
- assert abs(expected - computed) < tol med melding — 1 p. Det er her feil #11, likhetstegn på flyttall, koster hele poenget.
Om verdiområdet. Rekka konvergerer bare for , og saktere jo nærmere 1 du kommer. Prøver du med elleve ledd, treffer du bare fem desimaler:
from math import factorial, asin
def arcsin_rekke(x, N):
s = 0
for n in range(N+1):
s += factorial(2*n) * x**(2*n+1) / (4**n * factorial(n)**2 * (2*n+1))
return s
for x in [0.1, 0.3, 0.5, 0.7]:
print(f'{x:4.1f} {arcsin_rekke(x, 10):10.6f} {asin(x):10.6f}')Utskrift:
0.1 0.100167 0.100167
0.3 0.304693 0.304693
0.5 0.523599 0.523599
0.7 0.775394 0.775397Å nevne dette i én setning i besvarelsen — «testpunktet er valgt godt innenfor konvergensområdet» — er den typen presisjon som skiller en god besvarelse fra en riktig en. Det er ikke et krav for full score.
Merk hva som ikke kreves: ingen brukerinput, ingen utskrift fra funksjonene, ingen kommentarer utover eventuelle antakelser. Ni linjer kode for seks poeng.
Oppgaver: samme mønster, ny innpakning (~30 min for oppgave 1–6)
— naturlig pausepunkt —
Alle oppgavene under er på eksamensnivå. Innpakningen roterer bevisst, og både startverdi, sluttverdi og leddform varierer. Bruk oppskriftens sju steg på hver oppgave før du skriver en linje kode.
(Midtveisnivå, sjanger A — kodesporing, altså «hva skriver dette programmet ut?».) Hva skriver programmet ut? Skriv utskriften nøyaktig, med mellomrom og linjeskift slik terminalen viser den.
s = 0
for k in range(1, 4):
s += (-1)**k * k
print(k, s, end=' ')
print()med .
a) Skriv exp_rekke(y, N).
b) Skriv en testfunksjon for med toleranse 1e-10, og velg selv.
c) Hvorfor er det uproblematisk at er negativ her, selv om alle leddene i formelen ser positive ut?
a) Skriv ln1p_rekke(x, N).
b) Vis at rekka konvergerer langsomt ved å regne den ut for , og med , og sammenlign med math.log(1.2).
c) Hvilken toleranse ville du valgt i en testfunksjon med ?
a) Skriv cosh_rekke(x, N).
b) Skriv en testfunksjon mot math.cosh for .
c) Hva gir funksjonen for , og hvorfor er det en god kontroll?
a) Skriv atanh_rekke(x, N).
b) Skriv en testfunksjon for som faktisk går gjennom. Du må velge og tol som henger sammen, og begrunne valget i en kommentar.
c) Forklar hvorfor tol = 1e-10 med ville feilet, uten at koden er gal.
a) Skriv chebyshev(n, x) med en løkke.
b) Skriv en testfunksjon som sjekker mot de kjente formlene og for .
c) Hvorfor må den nye verdien regnes ut i en egen variabel før lagrene flyttes?
end og fortegnet det egentlige temaet.Oppgave 2 testet steg 1 i oppskriften — å skrive ledd-uttrykket som det står — og fellen med å legge til en fortegnsfaktor som formelen ikke har.
Oppgave 3 testet steg 2 og 3: startverdi 1 i stedet for 0, og et fortegn som skal være positivt først.
Oppgave 4 testet steg 6, indekseringskontrollen. Ser du 2n i eksponenten, skal du se 2n i fakultetet.
Oppgave 5 testet steg 5, og særlig at og toleransen henger sammen. Dette er det ene stedet i sjangeren der to riktige svar kan gi ulik uttelling: en begrunnet toleranse er verdt mer enn en tilfeldig.
Oppgave 6 testet steg 7, den andre malen. Legg merke til at rekurrensen ikke har noen akkumulator i det hele tatt — det er en helt annen løkke, og den forveksles lett med summeringsmønsteret under tidspress.
Oppgave 7–12: variantene som skiller (~25 min)
— naturlig pausepunkt —
De neste seks oppgavene ligner mest på det du møter når sjangeren er verdt seks poeng i stedet for fire: en «finn feilen», en «hvilken linje mangler», en rekke som styres av en toleranse i stedet for et antall ledd, og en rekurrens med en formel som varierer med leddnummeret.
Programmet skal summere og teste mot math.sinh. Det har fire feil.
import sinh from math
from math import factorial
def sinh_rekke(x, N):
s = 0
for n in range(N):
s += x**(2*n+1) / factorial(n)
return s
def test_sinh_rekke(x, N):
assert sinh_rekke(x, N) == sinh(x)a) Nevn de fire feilene og si hva hver av dem fører til.
b) Skriv den rettede koden.
(Midtveisnivå, sjanger E — «hvilken linje mangler?». Sjanger E er flervalgstypen der ett kall eller én linje er fjernet fra et ellers riktig program.) Funksjonen skal summere , men én linje er fjernet.
from math import factorial
def exp_rekke(x, N):
# HER MANGLER ÉN LINJE
for k in range(N+1):
s += x**k / factorial(k)
return sHvilken linje hører på plassen?
a) s = 1
b) s = 0
c) s = x
d) return s
I stedet for å oppgi antall ledd, ber oppgaven om at summeringen skal stoppe når leddet er blitt lite nok.
a) Skriv exp_tol(x, eps) som summerer rekka og stopper når absoluttverdien av det siste leddet er mindre enn eps. Funksjonen skal returnere både summen og antall ledd som ble brukt.
b) Kjør den for med eps lik 1e-4 og 1e-10, og sammenlign med math.exp.
c) Hvorfor må ledd settes til en verdi større enn eps før løkka?
Legg merke til at koeffisientene her avhenger av , i motsetning til i Chebyshev-formelen.
a) Skriv legendre(n, x) med en løkke.
b) Skriv en testfunksjon mot de kjente formlene og for .
c) Hva må du passe ekstra på når koeffisientene avhenger av leddnummeret?
(Midtveisnivå, sjanger D — «hvilke av påstandene er riktige?». Sjanger D er matche-typen der du avgjør om hver påstand holder.) Funksjonen under skal summere .
def rekke(x, N):
s = 0
for k in range(N+1):
s += x**k / factorial(k)
return sAvgjør for hver påstand om den er riktig eller gal.
| Rad | Påstand |
|---|---|
| 1 | Løkka går N+1 runder |
| 2 | s = 0 kan like gjerne stå inne i løkka |
| 3 | return s må stå utenfor løkka |
| 4 | Funksjonen krever from math import factorial for å virke |
| 5 | range(N+1) gir verdiene 1 til og med N+1 |
| 6 | Kalt med N = 0 returnerer funksjonen 1.0 |
a) Skriv sin_rekke(x, N).
b) Skriv en testfunksjon som sjekker to punkter: ett der du kjenner svaret uten å regne, og ett der fasiten hentes fra math.sin.
c) Skriv koden som lager en tabell over , sin_rekke(x, 5) og math.sin(x) for fem verdier av mellom 0 og 3, formatert med seks desimaler.
d) Hva ser du i tabellen når vokser, og hvorfor?
range(N), ulik indeksering i teller og nevner, og return inne i løkka.Oppgave 8 var sjanger E på sitt enkleste: én linje er fjernet, og de tre gale alternativene er alle plausible startverdier. Slike oppgaver koster ett poeng og tar tretti sekunder — de er blant de billigste på hele settet.
Oppgave 9 byttet ut for med while. Formuleringen «summer til leddet er mindre enn» er signalet: da er antall runder ukjent, og du må telle leddnummeret selv.
Oppgave 10 var rekurrensen med variable koeffisienter. Signalet er at leddnummeret står inne i formelen, og da må løkkevariabelen brukes der — ikke funksjonsargumentet.
Oppgave 11 var midtveisformatet for denne sjangeren: ingen kode skal skrives, bare påstander vurderes. Legg merke til at fire av påstandene handler om løkkegrenser og plassering — det er der poengene ligger.
Oppgave 12 var den fulle formen sjangeren har når den er verdt seks poeng eller mer: sum, test, tabell og en kort forklaring. Klarer du den uten å slå opp, er sjangeren i boks.
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
Bokas kode for oppgaven «skriv en funksjon som summerer en rekke, og skriv en testfunksjon til den». Sjangeren står i 8 av 8 gjennomgåtte avsluttende sett og teller 4–6 poeng, delt i del-a (summen) og del-b (testen). Innpakningen roterer år for år; øvelsen er identisk.
Alt til høyre for summetegnet oversettes tegn for tegn til Python, uten forenkling. (2n)! blir factorial(2*n), blir 4**n, blir factorial(n)**2. Forenkling underveis er en av de vanligste kildene til feil under tidspress.
Startverdien står under tegnet og blir første argument i range; sluttverdien står over og blir stoppverdien pluss én, fordi range er eksklusiv i den enden. «De første leddene» er den ene formuleringen som gir range(N).
s = 0, løkka, s += ledd, return s. Rekkefølgen og innrykkene er hele strukturpoenget, og skjelettet er verdt å skrive ned før du har forstått formelen.Ingen argumenter, expected fra en uavhengig kilde, computed fra funksjonen, tol valgt etter metoden, og assert abs(expected - computed) < tol, msg. Testfunksjonen kan skrives selv om del-a ikke ble ferdig — du vet hva funksjonen skal gjøre.
Står 2*n+1 i eksponenten, skal factorial(2*n+1) stå i nevneren. Ulik indeksering gir en helt annen funksjon, og kontrollen tar to sekunder.
Basistilfelle med if øverst, to lagervariabler satt til startverdiene, hjelpevariabel for den nye verdien, og løkke fra 1 til . Merk at rekurrensen ikke har noen akkumulator i det hele tatt — det er en annen løkke enn summeringsmønsteret.
expected skal komme fra en annen vei enn koden: math, en kjent eksplisitt formel som , eller håndregning. Gjentar fasiten koden, tester du ingenting.To forskjellige ting med lignende navn. eps er stoppgrensen i en while-variant og styrer hvor mange ledd som tas med. tol er toleransen i testfunksjonen og styrer hvor stort avvik testen godtar. En oppgave kan bruke begge samtidig.
Når oppgaven sier «summer til leddet er mindre enn», er antall runder ukjent, og while abs(ledd) > eps: er riktig løkke. Leddnummeret telles opp for hånd med k += 1, og startverdien på ledd må være større enn eps så løkka i det minste får gå én gang.
En while-løkke over rekkeledd stopper fordi leddene går mot null. For vinner fakultetet til slutt, uansett hvor stor er. Én setning om at løkka terminerer er verdt å ta med når oppgaven bruker while.
Første ledd i eksponentialrekka er 1, i partallsrekkene 1, og i oddetallsrekkene selv. Kontrollen tar ti sekunder, krever ingen datamaskin, og avslører de fleste indekseringsfeil.
Noen rekker gjelder bare for visse : arcus sinus og invers tangens hyperbolicus krever , og konvergerer saktere jo nærmere 1 du kommer. Eksponential-, sinus- og cosinusrekkene konvergerer for alle , men trenger flere ledd når er stor.
Sensor gir uttelling steg for steg: akkumulator, løkkegrense, ledd-uttrykk, return, testfunksjon. Riktig struktur med ett feil ledd gir de fleste poengene; riktig ledd uten struktur gir få. Skriv derfor skjelettet først.
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.