Pythons indbyggede filhåndtering er en af sprogets reelle styrker — ingen imports nødvendige
til grundlæggende læse/skrive-operationer, og API'et er rent nok til at lære på en eftermiddag. Men der er et reelt
hul mellem tutorialversionen og det, du faktisk ville sende til produktion. Tutorialversionen åbner en fil, læser den
og lukker den. Produktionsversionen håndterer kodningsuoverensstemmelser, der stille korrumperer data, stier der
virker på macOS men eksploderer på Windows, og logfiler der stille æder al din hukommelse, hvis du kalder
read()
på en 2 GB fil. Denne artikel dækker de mønstre, der holder — ikke bare den glade vej.
with-sætningen — brug den altid
Hvert filhåndteringseksempel i Python bør bruge en
konteksthåndterer
— den with-blok, der sikrer, at filen lukkes selv hvis en undtagelse rejses midt i læsningen.
En konteksthåndterer er et objekt, der definerer, hvad der sker ved ind- og udtræden fra en with-blok;
for filer betyder udtræden, at close() kaldes automatisk. Her er, hvorfor det betyder noget i praksis:
# ❌ Manual close — works until it doesn't
f = open('app.log')
data = f.read() # if this raises an exception...
f.close() # ...this line never runs. File handle leaks.
# ✅ Context manager — close() is guaranteed
with open('app.log') as f:
data = f.read()
# file is closed here, no matter what happened inside the blockPå langkørende servere er dette ikke akademisk — lækage af filhåndtag medfører til sidst
OSError: [Errno 24] Too many open files. with-sætningen koster intet
og forhindrer den fejlklasse fuldstændigt. Brug den overalt.
Læsning af filer — fire måder, ét rigtigt værktøj hver gang
Python giver dig adskillige metoder på et filobjekt, og det er vigtigere at vælge det rigtige, end de fleste tutorials indrømmer:
f.read()— læser hele filen ind i en enkelt streng. Fint til små konfigurationsfiler, farligt til store.f.readline()— læser én linje ad gangen og fremfører den interne peger. Nyttigt, når du har brug for manuel kontrol over iteration.f.readlines()— læser alle linjer ind i en liste. Praktisk, men indlæser stadig hele filen i hukommelsen.for line in f:— iteratorprotokollen. Læser én linje ad gangen uden at indlæse den fulde fil. Dette er den, du skal nå frem til som standard.
Her er et realistisk eksempel: læsning af en .env-lignende konfigurationsfil og omdannelse af den
til en ordbog. Dette er den slags ting, du faktisk skriver, ikke en kunstig "læs hello.txt"-demo:
from pathlib import Path
def load_config(path: str) -> dict:
"""Read a key=value config file, ignoring comments and blank lines."""
config = {}
with open(path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' not in line:
continue
key, _, value = line.partition('=')
config[key.strip()] = value.strip()
return config
# Usage
settings = load_config('config/app.conf')
db_host = settings.get('DB_HOST', 'localhost').strip()-vanen: Når du læser linjer, inkluderer hver linje undtagen
den sidste et efterfølgende \n (og på Windows, \r\n). Kald
line.strip() for at fjerne begge. Hvis du kun vil fjerne linjeskiftet og ikke foranstående
mellemrum, brug line.rstrip('\n') i stedet.Skrivning og tilføjelse — kend hvilken tilstand der ødelægger data
Det andet argument til
open()
er tilstanden. To tilstande snubler folk over igen og igen:
'w'— skrivetilstand. Åbner filen til skrivning. Hvis filen allerede eksisterer, afkortes den til nul bytes øjeblikkeligt — inden du skriver et eneste tegn. Dette er stille datadestruktion, hvis du åbner den forkerte sti.'a'— tilføjelsestilstand. Åbner filen og flytter skrivepegeren til slutningen. Eksisterende indhold berøres aldrig. Nye skrivninger går efter det, der allerede var der.
Et godt brugstilfælde til tilføjelsestilstand er at skrive en struktureret logfil med tidsstempler. Her er et mønster, der er nyttigt i scripts og små tjenester:
import datetime
LOG_FILE = 'logs/pipeline.log'
def log_event(level: str, message: str) -> None:
timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
line = f"[{timestamp}] {level.upper()}: {message}\n"
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(line)
log_event('info', 'Pipeline started')
log_event('warning', 'Retrying connection to database')
log_event('error', 'Failed to parse row 4821 — skipping')open(path, 'w') opretter filen, hvis den ikke eksisterer —
hvilket er praktisk — men den ødelægger stille også filen, hvis den eksisterer. En fejlskrevet
sti kan slette en produktionsfil uden nogen fejlmeddelelse. Hvis du ikke er sikker på, at filen bør
overskrives, tjek først med Path(path).exists() eller brug 'x'-tilstand, som
rejser FileExistsError i stedet for at overskrive.Kodning — den fejl, der bider alle til sidst
Dette er den absolut mest almindelige kilde til stille datakorruption i Python-filhåndtering.
Python 3's standardkodning, når du kalder open() uden at angive én, bestemmes
af locale.getpreferredencoding() — hvilket på Windows typisk er cp1252,
og på Linux/macOS normalt er UTF-8. Det betyder kode, der virker perfekt på din Mac,
kan stille forvanske eller crashe på en Windows-server, når filen indeholder et tegn uden for ASCII.
Løsningen er ét ekstra argument:
# ❌ Platform-dependent — works on Linux, corrupts on Windows
with open('customers.csv') as f:
data = f.read()
# ✅ Explicit UTF-8 — same behavior on every platform
with open('customers.csv', encoding='utf-8') as f:
data = f.read()
# For files exported from Excel on Windows — may have a BOM (byte order mark)
# utf-8-sig strips the BOM automatically on read
with open('export.csv', encoding='utf-8-sig') as f:
data = f.read()BOM-problemet er særligt almindeligt med CSV-filer eksporteret fra Microsoft Excel — filen
starter med et skjult \ufeff-tegn, der vises som , hvis det
læses med den forkerte kodning, eller får den første kolonneoverskrift til at se ud som
name i stedet for name. Brug af encoding='utf-8-sig'
håndterer det gennemsigtigt. Se
Python-codecs-dokumentationen
for den fulde liste af kodningsnavne.
encoding='utf-8' (eller
'utf-8-sig' til Excel-eksporter) til hvert open()-kald. Gør det til en
vane — det koster intet og eliminerer en hel kategori af miljøspecifikke fejl.Arbejde med stier — brug pathlib
Den gamle måde at bygge filstier i Python var strengsammenkædning eller
os.path.join(). Den moderne måde er
pathlib.Path,
tilgængelig siden Python 3.4 og fuldt moden siden 3.6. Den håndterer stiadskilletegn korrekt
på Windows og Unix uden at du tænker over det, og den erstatter en håndfuld
os.path-kald med læsbar attributadgang.
from pathlib import Path
# Build a path relative to the current script — works on Windows and Unix
base_dir = Path(__file__).parent
data_dir = base_dir / 'data'
input_file = data_dir / 'records.csv'
# Check existence before opening
if not input_file.exists():
raise FileNotFoundError(f"Input file not found: {input_file}")
# Create a directory (including parents) without error if it already exists
output_dir = base_dir / 'output' / 'reports'
output_dir.mkdir(parents=True, exist_ok=True)
# Iterate over all JSON files in a directory
for json_file in data_dir.glob('*.json'):
print(json_file.name) # just the filename: 'records.json'
print(json_file.stem) # filename without extension: 'records'
print(json_file.suffix) # extension: '.json'
print(json_file.parent) # parent directory as a Path
# The / operator builds paths — no os.path.join needed
report_path = output_dir / f"report_{input_file.stem}.txt"/-operatoren er ikke division her — Path tilsidesætter den til
at betyde stijoinning. Det læses naturligt og eliminerer anførsels- og adskillelsesproblemerne, der kommer
med strengbaseret stibyggning. En mere nyttig metode: path.read_text(encoding='utf-8')
er en genvej til åbn/læs/luk-mønsteret, når du blot vil have filens indhold som en streng.
Læsning af store filer uden at sprænge hukommelsen
Når en fil er lille — f.eks. under nogle megabytes — er f.read() eller
f.readlines() fint. Når det er en 500 MB serverlog eller en multi-gigabyte dataeksport,
er det at indlæse det hele i hukommelsen en hurtig vej til en MemoryError eller
et proceskill fra OS'et. Løsningen er linje-for-linje-iteration:
from pathlib import Path
from collections import Counter
def count_error_levels(log_path: str) -> dict:
"""
Process a large log file line by line.
Memory usage stays roughly constant regardless of file size.
"""
counts = Counter()
with open(log_path, encoding='utf-8') as f:
for line in f:
# Each line is fetched from disk as needed — not loaded all at once
if ' ERROR ' in line:
counts['error'] += 1
elif ' WARN ' in line:
counts['warning'] += 1
elif ' INFO ' in line:
counts['info'] += 1
return dict(counts)
results = count_error_levels('/var/log/app/server.log')
print(f"Errors: {results.get('error', 0)}, Warnings: {results.get('warning', 0)}")for line in f:-mønsteret virker, fordi Pythons filobjekt implementerer
iteratorprotokollen — det henter linjer fra disk én ad gangen ved hjælp af en intern buffer, så
hukommelsesforbruget er i bund og grund konstant uanset filstørrelse. For virkelig massive filer (titals af
gigabytes), hvor selv linje-for-linje-iteration ikke er hurtig nok, lader
mmap
dig hukommelsesmape filen og søge i den med regulære udtryk uden at læse den overhovedet —
men til de fleste brugstilfælde er linjeiteratoren alt, hvad du behøver.
Læsning og skrivning af JSON og CSV
To formater dukker op konstant i rigtig Python-arbejde, og begge har dedikerede stdlib-moduler der håndterer anførelse, escaping og struktur korrekt — parse dem ikke med streng-splits.
import json
import csv
from pathlib import Path
# --- JSON ---
# Reading
with open('config/settings.json', encoding='utf-8') as f:
settings = json.load(f) # parsed directly from the file object
# Writing (indent=2 gives readable output)
with open('output/results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
# --- CSV ---
# Reading
with open('data/customers.csv', encoding='utf-8-sig', newline='') as f:
reader = csv.DictReader(f) # each row is a dict keyed by header
for row in reader:
process_customer(row['email'], row['plan'])
# Writing
fieldnames = ['id', 'email', 'plan', 'created_at']
with open('output/export.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for record in records:
writer.writerow(record)Et par ting, der er værd at bemærke: send newline='', når du åbner CSV-filer —
csv-modulet
håndterer sine egne linjeslut, og at lade Pythons universelle newline-tilstand interferere forårsager
dobbelte tomme rækker på Windows. For
JSON,
lader ensure_ascii=False ikke-ASCII-tegn (accenterede bogstaver, CJK-tegn osv.) skrive
som de er i stedet for at blive escapet til \uXXXX-sekvenser — meget mere
læsbart output. Hvis du arbejder med JSON- eller CSV-data og vil inspicere eller transformere det
visuelt, er JSON Formatter og
CSV Formatter på dette websted gode komplementer til kode-tilgangen.
Fejlhåndtering — de tre undtagelser du vil se
Filoperationer fejler på forudsigelige måder. Håndtering af hvert tilfælde eksplicit giver dig fejlmeddelelser, der faktisk er nyttige i stedet for en generisk traceback:
import json
from pathlib import Path
def load_json_config(path: str) -> dict:
"""
Load a JSON config file with explicit error handling.
Returns the parsed config or raises with a clear message.
"""
config_path = Path(path)
try:
with open(config_path, encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
raise FileNotFoundError(
f"Config file not found: {config_path.resolve()}\n"
f"Create it or set CONFIG_PATH to the correct location."
)
except PermissionError:
raise PermissionError(
f"No read permission on {config_path.resolve()}\n"
f"Check file ownership and mode (chmod 644 on Linux)."
)
except UnicodeDecodeError as e:
raise ValueError(
f"Encoding error reading {config_path}: {e}\n"
f"Try opening with encoding='utf-8-sig' if the file came from Windows."
)
except json.JSONDecodeError as e:
raise ValueError(
f"Invalid JSON in {config_path} at line {e.lineno}, col {e.colno}: {e.msg}"
)De fire undtagelser dækker næsten alle reelle fejltilstande: filen eksisterer ikke,
du har ikke tilladelse, kodningen er forkert, eller indholdet er misdannet. Hver besked
fortæller den næste udvikler (eller dig klokken 2 om natten) præcis, hvad der gik galt, og hvor man skal se.
At fange en nøgen Exception og printe "noget gik galt" er ikke nyttig fejlhåndtering —
det flytter bare forvirringen nedstrøms.
FileNotFoundError
og returnere None eller et standard-dict. Vælg én adfærd pr. funktion
og dokumenter den.Opsummering
Den korte version af alt ovenstående: brug altid with-blokke, send altid
encoding='utf-8', brug pathlib.Path til stibyggning, og
iterer linjer i stedet for at læse hele filer, når størrelsen er ukendt. Disse fire vaner eliminerer
det store flertal af filhåndteringsfejl, inden de når produktion.
Til dybere læsning: Python-tutorial-sektionen om læsning og skrivning af filer dækker det grundlæggende grundigt. pathlib-dokumentationen er værd at bogmærke — det er en af de mest nyttige dele af stdlib, og de fleste Python-udviklere underudnytter det. csv-moduldokumentationen og json-moduldokumentationen har begge gode eksempler til kanttilfælde (brugerdefinerede skilletegn, streaming JSON osv.), der er værd at læse, hvis du arbejder med disse formater regelmæssigt.