Filen som minns

Inventoryt glömmer fortfarande

I Inventoryt som glömmer löste du ett mysterium: listan som var tom varje varv. Lösningen var att skapa den utanför loopen i stället för inuti.

Men prova det programmet igen. Lägg till tre föremål, avsluta, och starta det på nytt. Inventoryt är tomt.

Den här gången är det inget du kan flytta på. Sedan Lådor i RAM & datatyper har all data ditt program hanterat legat i RAM, och RAM håller sina laddningar bara så länge ström flödar genom kretsen. Stängs programmet dräneras de. Varje variabel du någonsin skrivit i den här kursen har varit temporär av konstruktion, inte av misstag.

Vill du att något ska överleva måste det lämna RAM och skrivas ned någon annanstans. Den platsen är filsystemet, på hårddisken eller SSD:n. Det är långsammare än RAM, betydligt, men det är det enda minne som fortfarande finns kvar när strömmen varit borta.

Så här ser vägen dit ut: i den här delen bygger du verktyget på en enkel anteckningsfil, där det är lätt att se vad som händer. I nästa del sätter du in det i inventoryt på riktigt. Verktyget först, tillämpningen sedan.

Filen som överlever

Det du är ute efter kallas Persistent lagring Lagring som överlever att programmet avslutas och strömmen bryts. Filer på hårddisk och SSD är persistenta, till skillnad från RAM som är flyktigt. Se: Filen som minns . Persistent betyder ihållande eller envis, alltså något som håller i sig, och det är precis vad som skiljer disken från RAM: den envisas med att komma ihåg även när strömmen är borta. Det är samma sak som händer när du sparar i VS Code, texten lämnar editorns arbetsminne och läggs på disken.

with open(), filen stängs åt dig

Att öppna en fil är att be operativsystemet om tillgång till den. Den tillgången måste lämnas tillbaka när du är klar, annars kan filen bli låst eller innehållet ofullständigt.

En with open() Öppnar en fil och stänger den automatiskt när blocket tar slut, även om koden inuti kraschar. Använd den alltid, då kan du aldrig glömma att stänga. Se: Filen som minns sköter det åt dig. Filen stängs när blocket tar slut, oavsett hur det tar slut, även om koden inuti kraschar. Du behöver aldrig tänka på det, och det är hela skälet att alltid skriva så:

with open("anteckningar.txt", "w", encoding="utf-8") as fil:
    fil.write("Ringa tandläkaren
")

encoding="utf-8" talar om hur tecknen ska lagras. Den är värd ett eget avsnitt, och det kommer strax.

Fillägen bestämmer vad som händer

Anropet har tre delar, och det är värt att kunna dem utantill:

with open(filnamn, filläge, encoding="utf-8") as fil:

Först filnamnet, filen du vill åt. Sedan Filläge Det andra argumentet i open(). 'r' läsning, 'w' skrivning, 'a' tillägg. Se: Filen som minns , som avgör vad som får hända med den. Sist encoding, som bestämmer hur tecknen lagras. Efter as kommer namnet du använder för filen inuti blocket, fil är konventionen.

Filläget är den detalj som orsakar mest oavsiktlig dataförlust:

  • 'r' läser. Filen måste finnas, annars blir det fel.
  • 'w' skriver. Skapar filen om den saknas, och tömmer den om den finns. Tyst.
  • 'a' lägger till. Skapar filen om den saknas, och skriver i slutet om den finns.

Skillnaden mellan 'w' och 'a' är skillnaden mellan att skriva om ett dokument och att skriva vidare i det.

Det finns fler lägen, bland annat sådana som både läser och skriver i samma öppning. Du behöver dem inte i den här kursen, och de är knepigare än de ser ut: 'w+' tömmer filen precis som 'w' gör. Ska ett program både läsa och skriva är det nästan alltid tydligare att göra det i två separata öppningar, en för vardera, precis som du strax gör med ladda() och spara().

Encoding, hur bokstäver blir bytes

I Noll eller ett såg du att bokstaven A är samma bitmönster som talet 65. En fil innehåller alltså inga bokstäver, den innehåller tal, och att A blir just 65 är ingen naturlag. Det är en överenskommelse: en teckenkodning, en tabell som säger vilket tal som betyder vilket tecken.

Länge hade olika länder olika tabeller, och de var oense. Samma tal betydde å i den ena och något helt annat i den andra, så en fil öppnad med fel tabell blev obegriplig:

Fråga användaren om åldern

Det är ingen trasig fil. Det är rätt tal, lästa med fel tabell.

Lösningen heter UTF-8 och är i dag standard nästan överallt, en tabell som rymmer alla världens skriftsystem. Skriver du ut encoding="utf-8" säger du åt Python att använda just den i stället för att gissa utifrån datorns inställningar. Det är hela poängen med parametern.

Att skriva och läsa

När du sparar en lista skriver du en rad per element, och radbrytningen \n är det som håller isär dem:

anteckningar = ["Ringa tandläkaren", "Lämna tillbaka boken", "Köpa kaffe"]

with open("anteckningar.txt", "w", encoding="utf-8") as fil:
    for rad in anteckningar:
        fil.write(rad + "
")

Vid läsning kommer allt tillbaka som en enda sträng med radbrytningar inbakade. splitlines() delar den vid varje radbrytning och ger dig listan tillbaka, utan att \n följer med:

with open("anteckningar.txt", "r", encoding="utf-8") as fil:
    anteckningar = fil.read().splitlines()

Editor

Redo.

Lägg märke till att den avslutande radbrytningen inte ger något tomt element på slutet. splitlines() hanterar det åt dig, vilket är precis därför den är att föredra framför att dela strängen för hand.

Den första körningen

Ett program som laddar sin data vid start har ett problem första gången det körs: filen finns inte än. Med filläget 'r' betyder det FileNotFoundError Uppstår när du öppnar en fil med 'r' och filen inte finns. Hanteras med try/except FileNotFoundError. Se: Filen som minns , och du känner igen lösningen från Kraschsäkring:

try:
    with open("anteckningar.txt", "r", encoding="utf-8") as fil:
        anteckningar = fil.read().splitlines()
    print(f"Laddade {len(anteckningar)} anteckningar.")
except FileNotFoundError:
    anteckningar = []
    print("Inga sparade anteckningar. Startar tomt.")

Det viktiga är vad except-grenen gör. Den varken kraschar eller klagar, den bestämmer vad som gäller när det inte finns någon historik: en tom lista. En saknad fil är inte ett fel här, det är en förstagångsanvändare.

Det kompletta mönstret

Sätter du ihop delarna får du två funktioner som hör ihop, en som hämtar tillbaka och en som lägger undan:

FILNAMN = "anteckningar.txt"

def ladda():
    try:
        with open(FILNAMN, "r", encoding="utf-8") as fil:
            return fil.read().splitlines()
    except FileNotFoundError:
        return []

def spara(rader):
    with open(FILNAMN, "w", encoding="utf-8") as fil:
        for rad in rader:
            fil.write(rad + "
")

Filnamnet ligger i en konstant överst i stället för att upprepas i båda funktionerna. Byter du namn på filen finns det då ett enda ställe att ändra på. Att ladda() returnerar en tom lista i stället för None är också ett medvetet val: den som anropar får alltid en lista tillbaka och behöver aldrig kontrollera vilket.

Det här är receptet Persistent lagringMönsterkortet. Formen är alltid densamma, bara namnen och innehållet byts ut mot det just ditt program ska minnas.

Uppgift: Filen som överlever omstarten

Skapa filtest.py i din kursmapp. Den här uppgiften körs i VS Code och inte i sandlådan, för hela poängen är att se filen dyka upp i filträdet och ligga kvar där.

  1. Skriv två rader till test.txt med filläget 'w'. Kör programmet och öppna test.txt i VS Code.

  2. Läs tillbaka filen med .splitlines() och skriv ut listan. Kontrollera att inga \n följde med in i elementen.

  3. Kör programmet igen med ett annat innehåll, fortfarande med 'w'. Öppna filen. Vad hände med det du skrev först?

  4. Byt till 'a' och kör två gånger till. Öppna filen igen och jämför med förra gången.

  5. Bygg om filtest.py till mönstret: en ladda() som klarar att filen saknas och en spara(rader). Radera test.txt från filträdet och kör programmet, det ska starta tomt utan att krascha.

  6. Gå till Source Control-panelen i VS Code, stagea filen, skriv ett meningsfullt commit-meddelande och pusha.

Vad händer om

Du öppnar test.txt i VS Code, skriver in en rad för hand och sparar. Kör sedan programmet igen. Ser det din handskrivna rad? Vad säger det om vem som äger datan när den väl ligger i en fil?

Motivera & reflektera

Motivera varför ladda() returnerar en tom lista i stället för None när filen saknas. Vad hade den som anropar funktionen behövt göra annorlunda med den andra varianten?

Nu har du verktyget och har provat det på egen hand. I nästa del flyttar du mönstret till ett program du redan byggt.