Strenger


Basics

Se notatene fra kom i gang om strenger.

Fire måter å skrive strenger
# I kildekoden kan streng-verdier oppgis på fire ulike måter
print('apostrof')
print("hermetegn")
print('''trippel-apostrof''')
print("""trippel-hermetegn""")

# Hvilken variant som brukes har absolutt ingenting å si
print('foo' == "foo") # True

# Så hvorfor ha flere varianter?
# Svar 1: kompabilititet
# Svar 2: for å enklere skrive hermetegn og apostrof
print("Her er 'apostrof'")
print('Her er "hermetegn"')
print("""Her er både "hermetegn" og 'apostrofer'""")
print("Hvis vi kun bruker "hermetegn" går det galt")
Linjeskift og escape-sekvenser
# Et tegn med en bakstrek foran seg, som \n, er en escape-sekvens.
# Selv om det ser ut som to tegn, er det bare ett tegn når Python er
# ferdig med å lese kildekoden. I tilfellet \n er dette et linjeskift.

# Merk at de to setningene under gjør det samme
print("abc\ndef")  # \n er ett enkelt linjeskift
print("""abc
def""") # Trippel-hermetegn/apostrof tillater linjeskift uten escape-sekvens

print("""\
Du kan bruke bakstrek på slutten av en linje for å ekskludere
et påfølgende linjeskiftet i kildekoden. Dette er svært sjeldent
brukt, men et anvendelsesområde er som i dette eksempelet, på
starten av en lengre streng over flere linjer. På den måten kan
hele strengen bli skrevet inn med samme indentering (altså ingen
indentering).
""")

Flere escape-sekvenser:

print("Hermetegn i hermetegn-streng: \"")
print("Bakstrek: \\")
print("Linjeskift: [\n]")
print("Tab: [\t]")
print()

print("Denne teksten er skilt av tab'er, 3 per linje:")
print("abc\tdef\tg\nhi\tj\\\tk\n---")
print()

# En escape-sekvens telles som ett tegn
s = "a\\b\"c\td"
print("s =", s)
print("len(s) =", len(s))
Konvertering til strenger

Verdier som ikke er strenger kan konverteres til en streng-representason med bruk av funksjonene str og repr.

def print_string_conversion(x):
    print("type:", type(x))
    print(" str:", str(x))
    print("repr:", repr(x))
    print()

print("Vanligvis konverteres verdier til streng med str-funksjonen")
print("Å bruke repr-funksjonen gir for mange vanlige typer samme resultat")
print_string_conversion(10)
print_string_conversion(True)
print_string_conversion(2/11)

print("Men for strenger, viser repr oss whitespace og escape-sekvenser.")
print_string_conversion("   Mellomrom\ttab   ")
print_string_conversion("Linje\nskift")

# Generelt vil `repr` vise mer detaljert informasjon enn `str`.
# Hensikten med `str` er at resultatet skal være leselig, hensikten
# med `repr` er å gi oss presis informasjon.

print("For andre typer kan forskjellen være stor")
import datetime
today = datetime.datetime.now()
print_string_conversion(today)

Generelt vil repr vise mer detaljert informasjon enn str. Hensikten med str er at resultatet skal være leselig på en pen måte, mens hensikten med repr er å være presis.

Konvertering og formatering med f-strenger

F-strenger lar oss konvertere til en streng i kontekst av en større streng. Man angir en f-streng ved å sette bokstaven f foran hermetegnet som angir at den større kontekst-strengen begynner; deretter kan vi angi hvilke variabler/uttrykk vi vil konvertere til streng ved å bruke {} inne i kontekst-strengen. Vises best med et eksempel:

name = "Eva"
age = 23
print(f"{name} er {age} år gammel") # Eva er 23 år gammel

F-strenger kan også brukes til å formatere verdier – for eksempel kan man spesifisere hvor lang strengen skal være, eller hvor mange desimaler som skal vises for et flyttall. Formatering spesifiseres ved å legge til et kolon : og en formaterings-spesifikasjon etter selve variabelnavnet/uttrykket inne i krøllparentesene {}. Her er noen eksempler:

Minimum bredde

x = 10
s = "abc"
pi = 3.141592653589793

print("Standard-justert")
print(f"** {x:10} **")  # **         10 **
print(f"** {s:10} **")  # ** abc        **
print(f"** {pi:10} **") # ** 3.141592653589793 ** (pi er mer enn 10 tegn)
print()

print("Venstrejustert")
print(f"** {x:>10} **") # **         10 **
print(f"** {s:>10} **") # **        abc **
print()

print("Høyrejustert")
print(f"** {x:<10} **") # ** 10         **
print(f"** {s:<10} **") # ** abc        **
print()

print("Sentrert")
print(f"** {x:^10} **") # **     10     **
print(f"** {s:^10} **") # **    abc     **
print()

print("Fyll med nuller")
print(f"** {x:010} **") # ** 0000000010 **

Antall desimaler i et flyttall

x = 10
pi = 3.141592653589793

print("Nøyaktig 3 desimaler (.3f)")
print(f"x er ca {x:.3f}") # x er ca 10.000
print(f"pi er ca {pi:.3f}") # pi er ca 3.142
print()

print("Minimum bredde 10 og 3 desimaler (10.3f)")
print(f"{'x':3} er ca {x:10.3f}")   # x   er ca     10.000
print(f"{'pi':3} er ca {pi:10.3f}") # pi  er ca      3.142

Snarvei for å vise både uttrykk og evaluering

Der er relativt vanlig å ønske å se verdien av en variabel eller et uttrykk samtidig som man ønsker å skrive ut hvilket uttrykk/variabel det faktisk er snakk om. Til dette har f-strenger en snarvei ved å avslutte uttrykket med =.

# Noen variabler
x = 10
y = 42

# Uten bruk av snarvei! (ulempe: vi gjentar samme uttrykk flere ganger; da er
# det fort gjort å endre kun ett av dem senere, som kan føre til logiske feil)
print("x =", x)         # x =  10
print("x + y =", x + y) # x + y =  52

# BRA! (vi bruker f-strenger til å skrive ut både uttrykk og evalueringsverdi)
print(f"{x = }")         # x = 10
print(f"{x + y = }")     # x + y = 52
Konstanter
import string
print(string.ascii_letters)   # abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
print(string.ascii_lowercase) # abcdefghijklmnopqrstuvwxyz
print("-----------")
print(string.ascii_uppercase) # ABCDEFGHIJKLMNOPQRSTUVWXYZ
print(string.digits)          # 0123456789
print("-----------")
print(string.punctuation)     # '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
print(string.printable)       # siffer + bokstaver + tegn + whitespace
print("-----------")
print(string.whitespace)      # mellomrom + tab + linjeskift etc....
print("-----------")
Operasjoner og metoder

Noen grunnleggende operasjoner:

print("abc" + "def") # Konkatenasjon 
print("abc" * 3)     # Repetisjon
print(len("abc"))    # Lengde
print()

 # Medlemskap (sjekk om venstresiden finnes som substreng av høyresiden)
print("a" in "abc")  # True
print("bc" in "abc") # True
print("ac" in "abc") # False, 'ac' er ikke sammenhengde i 'abc'
print("A" in "abc")  # False, 'A' er ikke det samme som 'a'
print("" in "abc")   # True, den tomme strenger er alltid en substreng

En metode er en funksjon som kalles «på» et objekt/en verdi. Kallet utføres ved hjelp at et punktum mellom objektet og metode-navnet (se eksempler under). Ulike typer har ulike metoder tilgjengelig. Her er noen metoder på typen str:

# .upper og .lower endre teksten til bare store eller små bokstaver
s = "FooBar"
print(s)
print(s.lower())
print(s.upper())
print("---") 

# .replace bytter ut substrenger
print(s.replace("o", "ahr"))
print("hahahaha".replace("hah", "l"))
print("hahahaha".replace("hah", "h"))
print("---")

# .split() deler opp en streng i biter, og legger bitene i en liste
names = "Marshall,Rubble,Chase,Rocky,Zuma,Sky"
print(names)
print(names.split(","))
print("---")

# .join() limer sammen strenger med en limestreng
print("+".join(names.split(",")))
print(s.join("ABC"))
print("---")

# .strip() fjerner whitespace foran og bak
s = "   FooBar  \n "
print(s, len(s))
print(s.strip(), len(s.strip()))
print("---")

Flere metoder

# Kjør koden for å se en tabell av hva funksjonene returnerer
def print_cell(test):
    print(f"{str(test):9}", end="")

def print_row(s):
    print(f" {s:4}  ", end="")
    print_cell(s.isalnum())
    print_cell(s.isalpha())
    print_cell(s.isdigit())
    print_cell(s.islower())
    print_cell(s.isspace())
    print_cell(s.isupper())
    print()

def print_table():
    print("  s   isalnum  isalpha  isdigit  islower  isspace  isupper")
    for s in "ABCD,ABcd,abcd,ab12,1234,-123,1.0,    ,AB?!".split(","):
        print_row(s)

print_table()

Søking i strenger

print("Dette er et ran".count("et")) # 2
print("Dette er ETT ran".count("et")) # 1
print("-------")
print("Hunder og katter".startswith("Hun"))     # True
print("Hunder og katter".startswith("Hun der")) # False
print("-------")
print("Hunder og katter".endswith("er"))      # True
print("Hunder og katter".endswith("mer"))     # False
print("-------")
print("Hunder og katter".find("og"))          # 7
print("Hunder og katter".find("eller"))       # -1
print("-------")
print("Hunder og katter".index("og"))         # 7
print("Hunder og katter".index("eller"))      # Krasj!
Indeksering og beskjæring

Indeksering

s = "abcdefgh"
print(s)
print(s[0]) # a
print(s[1]) # b
print(s[2])
print()

length = len(s)
print(s[length - 1])
print(s[length]) # Krasjer (string index out of range)

Negative indekser

s = "abcdefgh"
print(s)
print(s[-1]) # Snarvei for s[len(s) -  1]
print(s[-2])

Beskjæring (engelsk: slicing)

# Beskjæring er som å indeksere, men vi kan hente ut mer enn ett tegn
#
# For en streng s vil s[<start>:<slutt>] evaluere til en streng som
# begynner med tegnet på indeks <start> i s og går opp til men ikke
# inkludert tegnet på indeks <slutt>.
#
# Minner dette om range(a, b)?

s = "abcdefgh"
print(s)       # abcdefgh
print(s[0:3])  # abc
print(s[1:3])  # bc
print()

print(s[2:3])  # c
print(s[3:3])  #         (ingenting -- dette er den tomme strengen (''))
print("---")

Beskjæring med default-verdier

s = "abcdefgh"
print(s)       # abcdefgh
print(s[3:])   # defgh
print(s[:3])   # abc
print(s[:])    # abcdefgh
print("---")

Beskjæring med steg

# Dette er ikke vanlig, men illustrerer slektskapet med range()
#
# For en streng s vil s[<start>:<slutt>:<steg>] beskjære strengen
# ved å begynne med tegnet på indeks <start>, og gå opp til og ikke
# inkludert <slutt> med avstand på <steg>

s = "abcdefgh"
print(s)             # abcdefgh
print(s[1:7:2])      # bdf
print(s[1:7:3])      # be
print("---")
print(s[0:len(s):2]) # aceg
print(s[::2])        # aceg
print(s[1::2])       # bdfh
print("---")
print(s[3:0:-1])     # dcb
print("---")

Å reversere en streng

s = "abcdefgh"

print("Dette virker, men er forvirrende:")
print(s[::-1])

print("Dette virker også, men er fremdeles forvirrende:")
print("".join(reversed(s)))

print("Beste løsning: skriv funksjon med selvforklarende navn.")
def reversed_string(s):
    return s[::-1]

print(reversed_string(s)) # klart og tydelig!
Løkker over strenger

Med indeksering

s = "abcd"
# Vanlig for-løkke over lengden til s
for i in range(len(s)):
    print(i, s[i])

# 0 a
# 1 b
# 2 c
# 3 d


print("---")
# Med enumerate blir det to iterander, både indeks og selve tegnet
for i, c in enumerate(s): 
    print(i, c)

# 0 a
# 1 b
# 2 c
# 3 d

Uten indeksering

s = "abcd"
for c in s:
    print(c)

# a
# b
# c
# d

Oppdeling med split

names = "Marshall,Rubble,Chase,Rocky,Zuma,Sky"
for name in names.split(","):
    print(name)

# Marshall
# Rubble
# Chase
# Rocky
# Zuma
# Sky


# Med indeksering
for i, name in enumerate(names.split(",")):
    print(i, name)

# 0 Marshall
# 1 Rubble
# 2 Chase
# 3 Rocky
# 4 Zuma
# 5 Sky

Oppdeling med splitlines

quotes = """\
Dijkstra: Simplicity is prerequisite for reliability.
Knuth: If you optimize everything, you will always be unhappy.
Dijkstra: Perfecting oneself is as much unlearning as it is learning.
Knuth: Beware of bugs in the above code; I have only proved it correct, \
not tried it.
Dijkstra: Computer science is no more about computers than astronomy is \
about telescopes.
"""

for line in quotes.splitlines():
    if line.startswith("Knuth"):
        print(line)

# Knuth: If you optimize everything, you will always be unhappy.
# Knuth: Beware of bugs in the above code; I have only proved it correct, not tried it.

Oppdeling med splitlines hvor man også inkluderer selve linjeskift-symbolet

paragraph = """\
Denne strengen
inneholder linjeskift.
"""

for line in paragraph.splitlines(keepends=True):
    print("Line:", repr(line))

# Line: 'Denne strengen\n'
# Line: 'inneholder linjeskift.\n'
Palindromer

Et palindrom er en streng som er lik fremlengs og baklengs.

# Det er mange måter å skrive en is_palindrome(s) -funksjon
# Her er flere. Hvilken er best?

def reversed_string(s):
    return s[::-1]

def is_palindrome1(s):
    return (s == reversed_string(s))

def is_palindrome2(s):
    for i in range(len(s)):
        if (s[i] != s[len(s)-1-i]):
            return False
    return True

def is_palindrome3(s):
    for i in range(len(s)):
        if (s[i] != s[-1-i]):
            return False
    return True

def is_palindrome4(s):
    while (len(s) > 1):
        if (s[0] != s[-1]):
            return False
        s = s[1:-1]
    return True

def is_palindrome5(s):
    if len(s) <= 1:
        return True
    if s[0] != s[-1]:
        return False
    return is_palindrome5(s[1:-1])

print(is_palindrome1("abcba"), is_palindrome1("abca"))
print(is_palindrome2("abcba"), is_palindrome2("abca"))
print(is_palindrome3("abcba"), is_palindrome3("abca"))
print(is_palindrome4("abcba"), is_palindrome4("abca"))
print(is_palindrome5("abcba"), is_palindrome5("abca"))
Representasjon i minnet

En streng representeres fysisk i datamaskinen (som alt annet) med en rekke av høye og lave spenninger vi kan tenke på som en sekvens av 1’ere og 0’ere. Hvordan en slik sekvens med 1’ere og 0’ere oversettes til ulike meningsbærende tegn og symboler avgjøres først og fremst av hvilken enkoding som brukes. Python benytter som standard en enkoding som heter UTF-8. I denne enkodingen matches hvert enkelt tegn med en såkalt unicode-verdi (også kalt ordinal) som er et heltall mellom \(0\) og \(1\:111\:998\). I skrivende stund er det \(149\:186\) av disse tallverdiene som faktisk har symboler knyttet til seg.

Vi kan se en oversikt over en del vanlige tegn og deres unicode-verdi på wikipedia. Vi kan for eksempel lese i tabellen at tegnet A har verdien 65 (i desimal), mens symbolet a har verdien 97.

# For å finne unicode-verdien (ordinal) til et tegn
c1 = "A"
u1 = ord(c1)
print(c1, u1)

# For å konvertere en ordinal tilbake til et tegn (character)
u2 = 97
c2 = chr(u2)
print(c2, u2)

# Skriv ut alfabetet
for i in range(ord("A"), ord("Z") + 1):
    print(chr(i), end="")
print()

Når man sammenligner to strenger, sammenlignes egentlig ordinal-verdien til tegnene i de to strengene. På grunn av rekkefølgen de engelske bokstavene har i unicode-tabellen, vil denne sammenligningen være «alfabetisk» dersom det ikke blandes mellom store og små bokstaver

print('"A" < "a":', "A" < "a") # True, siden 65 < 97 er True
print('"a" < "A":', "a" < "A") # False

def compare_lt(s1, s2):
    print(f"{repr(s1)} < {repr(s2)}: {s1 < s2}")

compare_lt("abc", "abx") # True, siden c har lavere ordinal enn liten x
compare_lt("abc", "abX") # False, siden c ikke har lavere ordinal enn stor X
print()
compare_lt("abc", "abc") # False, når verdiene er like vil ikke < gi True
compare_lt("ab", "abc") # True, den første strengen er prefiks for den andre
compare_lt("ac", "abc") # False, c har ikke lavere ordinal enn b

Eksempel på bruk av ordinaler: simpel kryptering.

# Vi kan utnytte ordinalene for å kryptere en melding
def encode(message, shift):
    message = message.upper()
    result = ""
    for c in message:
        ordinal = ord(c) - ord("A")
        ordinal = (ordinal + shift) % (ord("Z") - ord("A") + 1)
        result += chr(ord("A") + ordinal)
    return result

def decode(message, shift):
    return encode(message, -shift)

# Eksempel på kryptering
print(encode("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 3))
print(encode("HELLOCRYPTO", 5))

# Eksempel på dekryptering
print(decode("DEFGHIJKLMNOPQRSTUVWXYZABC", 3))
print(decode("MJQQTHWDUYT", 5))
Lese og skrive til fil
# Du kan kopiere read_file og write_file -funksjonene og bruke dem
# i din egen kode

def read_file(path):
    """ Given the file path (file name) of a plain text file, returns
    the content of the file as a string. """
    with open(path, "rt", encoding='utf-8') as f:
        return f.read()

def write_file(path, contents):
    """ Writes the contents to the file with the given file path. If
    the file does not exist, it will be created. If the file does
    exist, its old content will be overwritten. """
    with open(path, "wt", encoding='utf-8') as f:
        f.write(contents)

# Eksempler på bruk: skrive til fil
# Vi oppretter en fil foo.txt med et gitt innhold
contents_to_write = "Dette er en test!\nDet er bare en test!"
write_file("foo.txt", contents_to_write)

# Eksempel på bruk: lese fra fil
# Vi leser en fil foo.txt og lagrer innholdet som en streng
contents_read = read_file("foo.txt")

# Sjekk at lesing og skriving var vellykket
assert "Dette er en test!\nDet er bare en test!" == contents_read
print("Manuell test: sjekk at filen foo.txt ble opprettet "
      "(legg merke til i hvilken mappe), og kikk på innholdet.")

Funksjonene for å lese og skrive filer vil tolke filnavn/fil-stier relativt til den mappen skriptet blir startet fra – merk at dette ikke nødvendigvis er samme mappe hvor skriptet ligger. Når du kjører koden gjennom VSCode er start-mappen den mappen hvor du har åpnet VSCode, og ikke nødvendigvis den mappen hvor filen ligger (f. eks. dersom filen ligger i en undermappe).

Hjelp, filen blir ikke funnet

Når du kjører et Python-program, kjører programmet «i» en mappe som kalles current working directory (cwd). Du kan se hvilken mappe dette er med koden:

import os
cwd = os.getcwd()
print(cwd)

Denne mappen blir bestemt av hvilket program som starter python. F. eks. hvis du bruker VSCode for å starte python, vil cwd være samme mappe som VSCode er åpnet i (som altså ikke har noen sammenheng med hvilken mappe filen som kjøres ligger i).

Når python får beskjed om å åpne en fil, vil den tolke filstien som blir oppgitt relativt til cwd. For eksempel, hvis filstien er kun et filnavn, antas det at filen ligger i cwd.

La oss si at du bruker funksjonskallet read_file("foo.txt") og ønsker å åpne filen foo.txt, som ligger i samme mappe som python-filen du kjører, la oss si mappen labX. La oss videre tenke oss at labX i sin tur ligger i mappen inf100, og det er i den sistnevnte mappen du har åpnet VSCode. Da vil programmet krasje med en FileNotFoundError.

For å klare å åpne filen foo.txt ved å kjøre python fra VSCode, kan du gjøre ett av fire tiltak:

Det er teknisk sett mulig å endre cwd til å bli samme mappe som filen som kjøres ligger i programmatisk:

import os
directory_of_current_file = os.path.dirname(__file__)
os.chdir(directory_of_current_file) # endrer cwd

Dette kan kanskje gjøre ting lettere i utviklingsfasen og for raske og enkle formål, men er sannsynligvis ikke noe en erfaren programmerer ville ønsket seg; siden man da må flytte selve kildekodefilene bare fordi man vil bruke programmet i en annen mappe.