Bestanden lezen en schrijven
Lezen en schrijven met `with open`, wat r/w/a/x onderscheidt, de nieuwe regel aan het einde van elke regel, waarom `encoding` altijd wordt geschreven, en pathlib voor de kleine klusjes.
Hoe wil je leren?
Het probleem dat we oplossen
Alles wat we in tweeëntwintig hoofdstukken hebben gebouwd, verdween op het moment dat het programma eindigde.
basket = []
basket.append("pen")
print(basket)['pen']Draai het opnieuw en de mand is weer leeg. Lijsten, dictionaries, functies — alles leeft in het geheugen, en het geheugen duurt precies zolang als het programma.
Het vorige hoofdstuk ging over het delen van code tussen programma's. Dit hoofdstuk gaat over data — hoe één uitvoering zijn werk achterlaat voor de volgende uitvoering, of voor iemand anders.
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("first line\n")
fh.write("second line\n")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(fh.read(), end="")first line
second lineHet programma is geëindigd, en notes.txt is er nog steeds.
Aan het einde van dit hoofdstuk kun je
- Bestanden lezen en schrijven met
with open(...), en uitleggen waaromwith - Uitleggen wat
"r","w","a"en"x"van elkaar onderscheidt — vooral wat"w"vernietigt - Een bestand regel voor regel lezen, en omgaan met de
\naan het einde van elke regel - Uitleggen waarom
encoding="utf-8"altijd wordt geschreven pathlib.Pathgebruiken voor de kleine klusjes- Een
FileNotFoundErroren eenUnicodeDecodeErrorlezen
Voorkennis: Modules en imports.
Waarom with
Een bestand openen leent iets van het besturingssysteem, en een lening moet worden terugbetaald. Als je niet sluit, komt wat je hebt geschreven misschien nooit op de schijf terecht, en genoeg niet-gesloten bestanden stoppen het programma.
Een with-blok doet die terugbetaling zelf — het bestand wordt gesloten wanneer het blok eindigt, ook als er binnenin een exception wordt opgeworpen.
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(fh.closed)
print(fh.read())True
ValueError: I/O operation on closed file.(Vóór de foutregel print Python ook enkele traceback-regels; in dit hoofdstuk wordt alleen de laatste regel getoond.)
De naam fh bestaat na het blok nog steeds — scope werkt precies zoals eerder — maar het bestand is gesloten. In deze cursus wordt open() altijd met with geschreven.
Lezen
De hele inhoud in één keer krijg je met .read():
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(repr(text))
print(len(text))'first line\nsecond line\n'
23repr wordt bewust gebruikt, want een gewone print verbergt de regeleinden. Een bestand is één lang stuk tekst, met de regeleinden erin als \n — dat onthouden is de helft van dit hoofdstuk.
Eén regel per keer
.read() is een slechte keuze voor een groot bestand, want het tilt de hele boel het geheugen in. Over het bestand zelf lopen laat Python één regel per keer lezen:
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'Elke regel eindigt met \n. Dit is de meest voorkomende verwarring van allemaal — line == "pen" matcht niet, "pen\n" en "pen" zijn verschillende keys in een dictionary, en de reden is onzichtbaar. (int() en float() negeren witruimte en \n rond het getal zelf, dus int("3\n") geeft wél 3 — maar een string blijft precies zoals hij is, dus de \n blijft staan in vergelijkingen en in geprinte uitvoer.)
De eerste stap is dus bijna altijd .strip():
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
name, price, qty = line.strip().split(",")
print(f"{name:<5} {float(price) * int(qty):>8.2f}")pen 45.00
bag 850.00
ink 240.00.strip() dan .split(",") dan float() en int() — die volgorde komt keer op keer terug wanneer je bestanden leest. Alles wat uit een bestand komt is tekst, nooit getallen; hoofdstuk zeven zijn omzetting heb je hier ook nodig.
Wanneer je alle regels in één keer wilt, zijn er twee manieren:
with open("orders.txt", "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
print(lines)['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2'].splitlines() laat de regeleinden vallen. Er is ook .readlines(), maar die houdt de \n vast — en daar struikelen de meeste mensen over.
Schrijven, en één gevaarlijke letter
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("new\n")Het bestand bevatte eerder old content. Nu:
new"w" maakt het bestand leeg — op het moment van openen, voordat er iets is geschreven. Geen waarschuwing en geen weg terug.
Wat er staat behouden en aan het einde toevoegen: "a":
with open("notes.txt", "a", encoding="utf-8") as fh:
fh.write("new\n")old content
newEn weigeren überhaupt te schrijven als het bestand al bestaat: "x":
FileExistsError: [Errno 17] File exists: 'notes.txt'De vier modi in één regel: "r" leest, "w" wist en schrijft, "a" voegt toe aan het einde, "x" maakt alleen aan.
Denk na voordat je"w"typt. Mensen verliezen echt werk aan deze ene letter. Als het bestand dat je opent niet door jouw programma is gemaakt, denk dan na voordat je"w"schrijft.
De \n moet je zelf toevoegen
print breekt de regel voor je. write niet.
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)'penbag'De schoonste manier om een lijst regels te schrijven is dus:
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
fh.write("\n".join(rows) + "\n")'pen\nbag\n'"\n".join(rows) zet regeleinden tussen de regels, en de toegevoegde + "\n" eindigt het bestand op een nieuwe regel — dat is de gewone regel voor tekstbestanden, en wat veel tools verwachten.
encoding="utf-8" — altijd
Zonder encoding gebruikt Python de standaard van het besturingssysteem, en die verschilt per machine. Het resultaat is een programma dat werkt waar jij bent, en breekt waar iemand anders is.
Lezen met de verkeerde geeft:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byteSchrijf dus elke keer encoding="utf-8" wanneer je open() schrijft — voor lezen én voor schrijven. De enige uitzondering is binaire modus ("rb", "wb"), waar die vraag niet opkomt omdat er geen encoding is.
pathlib — een kort pad voor kleine klusjes
Voor een klein bestand kan het hele with-blok veel lijken.
from pathlib import Path
path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))
out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")True
3
doneread_text en write_text openen en sluiten het bestand intern, dus je hebt geen with nodig. Path kan meer — .exists(), .name, .suffix, .parent — en mappen worden met / samengevoegd, zoals in Path("data") / "orders.txt".
Wanneer welke? Path als je het hele bestand in één keer wilt, with open(...) als je regel voor regel leest. Eén ding om in gedachten te houden: write_text gedraagt zich altijd als "w" — als het bestand al bestaat, wordt de oude inhoud gewist zonder enige waarschuwing. Er is geen manier om toe te voegen; daar heb je with open(..., "a") voor.
Een compleet voorbeeld
orders.txt:
pen,15.0,3
bag,850.0,1
ink,120.0,2main.py:
"""Read an order file, total it, and write a report beside it."""
from pathlib import Path
TAX_RATE = 0.15
def read_order(path):
"""Each line is name,price,quantity. Blank lines are skipped."""
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
name, price, quantity = raw.split(",")
lines.append((name, float(price), int(quantity)))
return lines
def report(lines):
rows = []
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
rows.append("-" * 20)
rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
return rows
def main():
order = read_order(Path("orders.txt"))
rows = report(order)
Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"), end="")
print()
print("lines read:", len(order))
if __name__ == "__main__":
main()pen 3 51.75
bag 1 977.50
ink 2 276.00
--------------------
total 1305.25
lines read: 3Vier dingen die het bekijken waard zijn.
Drie functies met drie aparte taken. read_order leest vanaf de schijf, report rekent alleen, en main schrijft. Aan report wordt helemaal geen bestand gegeven — het krijgt een lijst en geeft een lijst terug. Testen vereist dus geen bestand, en in hoofdstuk achtentwintig is dat precies wat helpt.
Lege regels worden vooraf afgehandeld. Een enkele \n aan het einde van het bestand levert geen lege regel op — .splitlines() gooit hem weg. Maar in een bestand dat met de hand is bewerkt, is een echte lege regel (twee \n achter elkaar) in het midden of aan het einde heel gewoon, en zonder afhandeling zou raw.split(",") niet drie waarden opleveren en een ValueError opwerpen. if not raw.strip(): continue regelt het in één regel.
float(price) en int(quantity) zijn niet optioneel, want alles wat uit een bestand komt, komt als tekst aan. Laat je ze weg, dan herhaalt price * quantity stilletjes de string — het slechtste soort bug, het soort dat niet breekt.
Het rapport gaat naar een apart bestand. orders.txt wordt nooit met "w" geopend — het invoerbestand blijft intact. Dat is een gewoonte die het vormen waard is: schrijf niet naar wat je leest.
Wanneer het breekt
FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' Het bestand is er niet, of je draait het programma vanuit een andere map. Het pad wordt bepaald vanuit waar je het hebt gedraaid, niet vanuit de map van het script. Print Path("orders.txt").resolve() om te zien waar het kijkt.
UnicodeDecodeError: 'utf-8' codec can't decode byte ... Het bestand is met een andere encoding geschreven, of het is helemaal geen tekstbestand. Repareer de encoding, of open het met "rb" als het binair is.
ValueError: I/O operation on closed file. Het bestand werd buiten zijn with-blok gebruikt. Lees wat je nodig hebt binnenin het blok.
ValueError: not enough values to unpack Er is een regel die niet de komma's heeft die je verwachtte — vaak een lege regel. Sla lege regels over, en bij twijfel print(repr(raw)).
Ik heb het bestand geschreven maar het is leeg Het wordt gelezen voordat het with-blok klaar is. Wat je schrijft komt op de schijf terecht wanneer het bestand wordt gesloten.
Al mijn regels lopen samen in één regel write breekt geen regels. De "\n" moet je zelf toevoegen.
Alles in mijn bestand is weg Het is met "w" geopend. Gebruik "r" om te lezen en "a" om toe te voegen aan het einde.
Stap 4 van 6 — Voorspellen
Test je begrip
repr wordt gebruikt, dus er wordt niets verborgen. Wat wordt geprint?
# orders.txt
# pen,15.0,3
# bag,850.0,1
# ink,120.0,2
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))- A'pen,15.0,3' 'bag,850.0,1' 'ink,120.0,2'
- B'pen,15.0,3\nbag,850.0,1\nink,120.0,2\n'
- C'pen,15.0,3\n' 'bag,850.0,1\n' 'ink,120.0,2\n'
- D['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']
Het bestand wordt geopend met "w" maar er wordt niets geschreven. Wat wordt geprint?
# notes.txt already contains: important work
with open("notes.txt", "w", encoding="utf-8") as fh:
print("opened")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- Aopened 'opened\n'
- BEen `FileExistsError`
- Copened 'important work\n'
- Dopened ''
Twee dingen worden apart geschreven. Wat zit er in het bestand?
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)
with open("out.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- A'pen\nbag\n'
- B'pen\nbag'
- C'pen bag'
- D'penbag'
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Maak een bestand genaamd people.txt met op elke regel een naam en een getal, gescheiden door een komma — minstens vier regels, en laat er expres één lege regel in het midden staan.
Schrijf daarna summary.py met drie functies: één die het bestand in een lijst van paren leest, één die de lijst omzet in regels tekst (wie het grootste getal heeft, en het totaal), en main die het naar summary.txt schrijft. Vergeet if __name__ == "__main__": niet.
Daarna vijf experimenten:
- Verwijder de
.strip(), en print dan het laatste deel van elke regel metrepr(). Werptint()een fout? (Niet —int("10\n")geeft10.) Waar zit de\ndan verstopt, en wat gebeurt er op de lege regel? - Verwijder een getal uit één regel zodat de komma er nog wel is maar het getal niet. Lees de melding.
- Verander
"w"in"a"inmain, en draai het programma daarna drie keer. Wat is er metsummary.txtgebeurd? - Probeer
people.txtmet"w"te openen... doe dat niet. Maak een kopie en doe het met die in plaats daarvan, en kijk daarna wat er nog over is. - Print
Path("people.txt").resolve(), en draai het programma daarna vanuit een andere map. Verandert het pad?
Die laatste verklaart een fout die bijna iedereen één keer maakt: het programma werkte, en daarna werd het vanaf een andere plek gedraaid en wierp het een FileNotFoundError.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken