3.3 Strenger: split, strip, join, slicing og bygging
Strengmetodene som parser hver fillinje og bygger utdata — split, strip, join, slicing og test-metodene.
- Sjanger F — parsing, altså å gjøre en tekstlinje om til de verdiene den inneholder. Hver eneste programmeringsoppgave begynner med at en fillinje deles opp i felt. Ledd for ledd er det 5–10 % av settet, og det er den delen resten av oppgaven står på.
- Sjanger B — kodesporing, altså «hva skriver dette programmet ut?». Strengutsnitt med steg (tekst[::2], tekst[::-1]) er en fast innpakning, verdt 1–3 % per deloppgave.
To feil går igjen så ofte at de har egne numre i bokas feilkatalog: #2, å glemme .strip() slik at linjeskiftet henger med i det siste feltet, og #3, å dele på feil skilletegn. Begge gir kode som nesten virker — og det er verre enn kode som stopper.
Prioritet: dette må sitte (høyeste av bokas tre nivåer «må sitte», «må kunne», «bør kjenne til»). Kapitlet tar ca. 50 minutter og er delt i fire løkker med tidsanslag; det er et naturlig pausepunkt etter parsingdelen.
- kap. 3.1 — lister, indeksering og utsnitt. Strenger indekseres og deles opp på nøyaktig samme måte, så alt du kan der, kan du her.
- kap. 1.1 — konvertering med int() og float().
- kap. 1.3 — løkker, for å gå gjennom tegn eller felt.
Vil du se det samme stoffet i en annen innpakning, dekker Strenger strengmetodene og utsnitt i et parallelt emne.
Løkke 1 — strengen er en sekvens av tegn (~10 min)
En streng er tekst: et navn, en linje fra en fil, et tall som ennå ikke er konvertert. Du skriver den mellom fnutter, enkle eller doble — Python skiller ikke på dem, og boka bruker enkle.
Det viktigste å ta med seg er at en streng oppfører seg som ei liste med tegn. Den har lengde, den har indekser fra 0, og du kan ta utsnitt av den. Én forskjell finnes, og den er stor: en streng kan ikke endres.
En sekvens av tegn, skrevet mellom fnutter: 'Hamsun'. Tegnene har indekser fra 0 og oppover, akkurat som elementene i ei liste.
Strenger er uforanderlige: s[0] = 'X' er ikke lov og stopper programmet. Alle strengmetodene lager derfor en NY streng i stedet for å endre den gamle — det er den enkeltregelen som forklarer mest av oppførselen deres.
Antall tegn i strengen, mellomrom og skilletegn medregnet. Den tomme strengen '' har lengde 0.
Merk at len teller TEGN på en streng, men ELEMENTER på ei liste. len('12 7') er 4, mens len('12 7'.split()) er 2. Den forskjellen er en fast sporingsfelle.
Gir tegnet på plass i, som en streng av lengde 1 — Python har ingen egen tegntype. Negative indekser teller bakfra, så s[-1] er siste tegn.
Er indeksen for stor, stopper programmet med IndexError: string index out of range. Å tilordne, s[0] = 'X', gir derimot TypeError, fordi strenger ikke kan endres.
Gir en ny streng med tegnene fra og med a til men IKKE med b. Sluttgrensa er eksklusiv, som i lister og i range.
Utelates a eller b, går utsnittet fra begynnelsen eller til enden. Et utsnitt utenfor strengen gir ikke feil, bare en kortere streng — 'abc'[1:99] er 'bc'.
Tredje tallet er steget: s[::2] gir annethvert tegn fra begynnelsen, s[1::2] annethvert fra det andre tegnet.
Negativt steg går bakover, så s[::-1] er strengen snudd. Dette er den vanligste innpakningen av strengsporing på eksamen, og du må kunne skrive svaret tegn for tegn.
La ord = 'BIBLIOTEK'. Skriv ut lengden, første og siste tegn, de fire første tegnene, annethvert tegn, og ordet baklengs.
Skriv indeksene over bokstavene på kladdearket først: B er 0, I er 1, B er 2, og så videre til K som er 8.
ord = 'BIBLIOTEK'
print(len(ord))
print(ord[0], ord[-1])
print(ord[:4])
print(ord[::2])
print(ord[::-1])Utskrift:
9
B K
BIBL
BBITK
KETOILBIBTo detaljer å legge merke til. ord[:4] gir fire tegn, ikke fem — sluttgrensa er eksklusiv. Og ord[::2] plukker indeks 0, 2, 4, 6 og 8, altså fem tegn av ni.
Legg også merke til at utskriften ikke har fnutter rundt seg. print skriver strengens innhold, ikke måten den ble skrevet i koden.
(Innstegsoppgave, sjanger B — kodesporing.) Hva skriver programmet ut? Skriv alle fire linjene nøyaktig.
s = 'PROGRAM'
print(s[1:5])
print(s[::3])
print(s[-3:])
print(len(s[2:]))navn[0] = 'K' ser fornuftig ut, men stopper programmet med TypeError: 'str' object does not support item assignment. Strenger er uforanderlige.Vil du ha en endret versjon, bygger du en ny: navn = 'K' + navn[1:], eller du bruker .replace(). Det samme gjelder alle strengmetodene — tekst.upper() endrer ikke tekst, den gir deg en ny streng du må ta vare på:
tekst.upper() alene gjør ingenting. tekst = tekst.upper() gjør jobben.
Løkke 2 — å dele en linje i felt (~14 min)
Nå kommer den viktigste halvsiden i kapitlet. Hver programmeringsoppgave på eksamen begynner med en fil der hver linje er en post: Kristin Lavransdatter;Undset;14. Jobben din er å gjøre den linja om til de tre verdiene den inneholder.
To metoder gjør nesten hele arbeidet: .strip(), som fjerner skrot i endene, og .split(), som deler på et skilletegn. Rekkefølgen mellom dem er ikke likegyldig.
Deler strengen der sep står, og gir ei LISTE med bitene. Skilletegnet selv blir ikke med. 'a;b;c'.split(';') gir ['a', 'b', 'c'].
Står skilletegnet to ganger på rad, kommer det en tom streng imellom. Finnes det ikke i det hele tatt, får du ei liste med ett element — hele strengen — og ikke en feilmelding. Det er derfor feil skilletegn gir gal kode som kjører videre.
Deler på blanktegn — ett eller flere mellomrom, tabulatorer eller linjeskift — og hopper over tomme biter. '12 7 33'.split() gir ['12', '7', '33'] uansett hvor mange mellomrom det er mellom tallene.
Dette er formen du bruker når feltene er atskilt med mellomrom i ukjent antall. Merk at .split(' ') med ett mellomrom som argument IKKE gjør det samme: den lager tomme strenger mellom to mellomrom på rad.
Gir en ny streng uten blanktegn i BEGGE ender — mellomrom, tabulator og linjeskift. Tegn inne i strengen røres ikke.
Dette er den faste medisinen mot linjeskiftet som følger med hver linje du leser fra en fil. Uten .strip() blir siste felt '14\n' i stedet for '14', og sammenligninger med '14' slår feil selv om verdien ser riktig ut.
Med argument fjernes de oppgitte tegnene fra endene i stedet for blanktegn: '##sum##'.strip('#') gir 'sum'. Argumentet er en samling enkelttegn, ikke en delstreng som skal matche i sin helhet.
lstrip() og rstrip() gjør det samme, men bare fra venstre eller høyre side. rstrip() er nok mot linjeskift alene, men strip() er tryggere og brukes gjennomgående i boka.
Dette er bokas mest brukte mønster. Det består av tre steg, og rekkefølgen er bindende:
1. linje.strip() først — fjern linjeskiftet mens det ennå står ytterst.
2. .split(sep) deretter — del på det skilletegnet fila faktisk bruker.
3. int() eller float() til slutt, på de feltene som skal regnes med.
Samlet: felt = linje.strip().split(';'), og deretter for eksempel dager = int(felt[2]).
Bytter du om på steg 1 og 2, havner linjeskiftet inni det siste feltet, og int(felt[2]) er da avhengig av at Python tilfeldigvis tåler mellomrom rundt tall. Det gjør den — men felt[2] == '14' blir usant, og en sammenligning som det er akkurat det oppgavene ber om.
Bibliotekets utlånsfil har linjer på formen tittel;etternavn;dager, hver med et linjeskift på slutten. Gjør de tre linjene om til en tabell, og skriv ut tittel og lånedager for hver.
Linjene er skrevet med \n bakerst her, akkurat slik de ville sett ut lest fra fil. Legg merke til at .strip() kommer før .split().
linjer = ['Kristin Lavransdatter;Undset;14\n',
'Sult;Hamsun;7\n',
'Naiv. Super;Loe;21\n']
tabell = []
for linje in linjer:
felt = linje.strip().split(';')
tabell.append(felt)
for rad in tabell:
print(rad[0], int(rad[2]))
print(tabell[1])Utskrift:
Kristin Lavransdatter 14
Sult 7
Naiv. Super 21
['Sult', 'Hamsun', '7']Siste linje viser hva som faktisk ligger i tabellen: tre TEKSTfelt, også det som ser ut som et tall. '7' er ikke 7 før du har konvertert.
Sløyfer du .strip(), blir siste felt '7\n'. int('7\n') virker fortsatt — Python tåler blanktegn rundt tall — men rad[2] == '7' blir usant, og en utskrift av feltet får et ekstra linjeskift. Det er feilkode #2, og den koster poeng hvert år.
felt[1], stopper da programmet med IndexError, langt fra der feilen er. Les eksempelfila i oppgaveteksten før du skriver skilletegnet.Tabulator som skilletegn. Noen filer bruker tabulator. I koden skriver du det som linje.split('\t') — bakstrek og t, ikke et ekte tabulatortegn du limer inn.
Feilkode #2 — glemt .strip(). Rammer alltid det SISTE feltet, fordi det er der linjeskiftet henger. Feilen dukker opp som en sammenligning som burde vært sann, men ikke er det.
Feilkode #1 — glemt konvertering. Alt som kommer fra .split() er tekst. '7' + '14' gir '714', og '96' > '120' er sant fordi tekst sammenlignes bokstav for bokstav. Konvertér med int() eller float() med det samme.
En treningslogg har linjer på formen dato mengde tid, atskilt med et ukjent antall mellomrom, for eksempel '12.03 5.2 31'.
a) Skriv parse_okt(linje) som returnerer ei liste med de tre feltene, der mengden er et desimaltall og tiden et heltall.
b) Forklar med én setning hvorfor .split() uten argument er riktigere her enn .split(' ').
Løkke 3 — å bygge tekst (~12 min)
— naturlig pausepunkt —
Motsatt vei er like vanlig: du har verdier og skal lage en linje av dem, enten for å skrive den ut eller for å lagre den i en fil.
Du kan skjøte sammen med +, men da må alt være tekst, og du må selv passe på skilletegnene. Har du ei liste, er join både kortere og tryggere.
'a' + 'b' gir 'ab' — en ny streng der den andre er skjøtt på den første. '-' * 20 gir en strek av tjue bindestreker, nyttig som skillelinje i utskrifter.Begge sider av + må være strenger. 'sum: ' + 42 stopper programmet med TypeError; du må skrive 'sum: ' + str(42). Dette er den eneste plassen str er obligatorisk i dette kapitlet.
Setter sammen elementene i lista til én streng, med sep mellom hvert. ';'.join(['a', 'b', 'c']) gir 'a;b;c', og ', '.join(...) gir den kommaseparerte formen som ser pen ut i en utskrift.
Merk formen: det er skilletegnet du kaller metoden PÅ, og lista er argumentet — omvendt av det de fleste gjetter. Alle elementene må være strenger; tall må konverteres med str() først. join er den eksakte motsatsen til split.
Gir en ny streng der ALLE forekomster av delstrengen gammel er byttet ut med ny. Originalen står urørt, siden strenger ikke kan endres.
Finnes ikke gammel, returneres strengen uendret — metoden stopper aldri programmet. s.replace(' ', '') er en rask måte å fjerne ALLE mellomrom på, også de inne i strengen, i motsetning til strip() som bare tar endene.
Gjør de tre parsete bokradene om til utskriftslinjer igjen: først en semikolonlinje slik den ville sett ut i fila, så en pen linje på formen «Sult av Hamsun (7 dager)».
join krever at alt er tekst, så tallet må gjennom str() på vei tilbake.rad = ['Sult', 'Hamsun', 7]
tekst = [rad[0], rad[1], str(rad[2])]
print(';'.join(tekst))
print(rad[0] + ' av ' + rad[1] + ' (' + str(rad[2]) + ' dager)')Utskrift:
Sult;Hamsun;7
Sult av Hamsun (7 dager)Den andre linja viser hvorfor + blir tungvint: fire skjøter og fire str-kall for én setning. I kap. 3.4 møter du formatert utskrift, som gjør nøyaktig det samme med langt mindre skriving.
Legg merke til at ';'.join(...) er den eksakte motsatsen til .split(';'): den ene setter sammen, den andre deler opp.
Hva skriver programmet ut? Vær nøye med mellomrom.
navn = ' Nord-Odal '
print(len(navn))
print(len(navn.strip()))
print(navn.strip().replace('-', ' '))
print(navn.upper().strip())Løkke 4 — å lete og å teste (~12 min)
Til slutt et lag med metoder du bruker når du skal finne noe i teksten, eller sjekke hva slags tegn den består av.
Testmetodene er særlig nyttige før en konvertering: int('abc') stopper programmet, mens 'abc'.isdigit() bare svarer False. Å spørre først er forskjellen på en robust løsning og en som krasjer på første rare linje.
Gir True hvis delstrengen finnes et sted i s, ellers False. Den ser etter en sammenhengende bit, så 'gram' in 'PROGRAM' er usant — store og små bokstaver er forskjellige tegn.
Dette er den enkleste og vanligste sjekken, og den stopper aldri programmet. Vil du se bort fra store og små bokstaver, sammenligner du 'gram' in 'PROGRAM'.lower().
Gir indeksen der delstrengen begynner første gang, eller -1 hvis den ikke finnes. Den stopper aldri programmet, og det er hele poenget med den.
-1 som «ikke funnet» er en gjenganger i faget — den samme svarformen brukes i sekvensielt søk senere i boka. Test alltid if s.find(x) != -1: og ikke if s.find(x):, for indeks 0 er også et gyldig treff.
Gir samme indeks som find, men stopper programmet med ValueError hvis delstrengen mangler i stedet for å gi -1.
Bruk find når du er usikker på om teksten er der, og index når den må være der og en manglende verdi er en ekte feil. På lister finnes bare index, ikke find.
Gir en NY streng med alle bokstavene som store. Originalen står urørt, så du må ta vare på resultatet: navn = navn.upper().
Tegn som ikke er bokstaver — sifre, mellomrom, bindestrek — går uendret gjennom. Metoden brukes når utdata skal se ensartet ut, for eksempel i en kolonneutskrift.
Gir en ny streng med alle bokstavene som små, og lar originalen stå.
Den vanligste bruken er å sammenligne uten å bry seg om store og små bokstaver: if svar.lower() == 'ja': godtar både Ja, JA og ja. Uten den ville et svar med stor forbokstav blitt regnet som noe annet.
Gir True bare hvis strengen er minst ett tegn langt og BARE består av sifre. Den tomme strengen gir False.
Merk at minustegn og punktum ikke er sifre: '-4'.isdigit() og '5.2'.isdigit() er begge False. Metoden er derfor en trygg vakt foran int(), men ikke foran float().
Gir True bare hvis strengen er minst ett tegn lang og BARE består av bokstaver. Den tomme strengen gir False.
Mellomrom er ikke en bokstav, så 'Nord Odal'.isalpha() er False. Det overrasker mange, og er en yndet distraktor i flervalgsoppgaver.
Gir True når strengen bare består av bokstaver og sifre, i hvilken som helst blanding. Den tomme strengen gir False.
Bindestrek, punktum og mellomrom er ingen av delene, så 'LN-2481'.isalnum() er False mens 'LN2481'.isalnum() er True. Metoden brukes til å luke ut felt med skilletegn i seg.
Ei liste med lånedager kommer fra en fil og kan inneholde skrot. Summér bare de verdiene som faktisk er hele tall, og tell hvor mange som ble forkastet.
isdigit() svarer i stedet for å stoppe, og det er nettopp derfor den brukes her.felt = ['14', '7', 'ukjent', '21', '']
total = 0
forkastet = 0
for f in felt:
if f.isdigit():
total = total + int(f)
else:
forkastet = forkastet + 1
print(total)
print(forkastet)Utskrift:
42
2Både 'ukjent' og den tomme strengen ble forkastet, uten at programmet stoppet. Uten sjekken ville int('ukjent') gitt ValueError: invalid literal for int() with base 10: 'ukjent', og resultatene fra de gyldige linjene hadde gått tapt.
Legg merke til at variabelen heter total og ikke sum: sum er navnet på en innebygd funksjon, og å bruke det som variabel ødelegger funksjonen resten av programmet (feilkode #14).
En strekkodeleser på biblioteket leverer linjer på formen LN-2481 der tallet etter bindestreken er løpenummeret.
a) Skriv lopenummer(kode) som returnerer løpenummeret som heltall.
b) Returnér -1 hvis strengen ikke inneholder en bindestrek, eller hvis det som står etter den ikke er et tall.
Hva skriver programmet ut? Skriv alle fire linjene nøyaktig, med hakeparenteser der de hører hjemme.
linje = 'NO1;78;410;\n'
felt = linje.strip().split(';')
print(felt)
print(len(felt))
print(felt[0][::-1])
print('-'.join(felt))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 Norges teknisk-naturvitenskapelige universitet. Dette er ikke offisielt studiemateriell. Les mer.