A diagnosztika tárgya a Northstar
data/raw/orders_raw.csv fájl: 2405 rendeléssorból áll, és
az 2.1 szerint elkülönített nyers mappa
tudatosan hibás példányát adja. Egy sor tizenöt mezőt tartalmaz; a
szerződés szerint öt extra duplikált sor, két hiányzó
distance_km és egy negatív basket_minor van
benne. A megfigyelési egység egy CSV-sor, a folyamat futása
szempontjából pedig egyetlen Python-processz.
A tanuló dolga, hogy egy kis parancssori ellenőrzőt futtasson, a kimenetet szétválassza, rögzítse a visszatérési kódot, majd memóriabecslést készítsen. Előzetes becslés: ha egy sor átlagosan 350 byte, akkor a teljes szöveges fájl
Egy listában tárolt verzió a Python string- és mutatóoverheadje miatt 1–1,5 MiB körül lehet. Ellenőrzésre a futás végén a hibák számának és az exit code-nak kell egyeznie: 8 észlelt sorhiba, 4-es saját hibakód. A pontos byte-méret gépfüggő, ezért csak nagyságrend és invariáns a cél.
A futó program két alapértelmezett kimeneti csatornát kap: a standard
kimenetet (stdout, standard output) és a standard
hibakimenetet (stderr, standard error). A Python
print() alapesetben a stdout-ra ír, a
sys.stderr-re irányított írás pedig a
stderr-re. A szétválasztás nem kozmetika: a tudományos
futásnaplóban az adatszerű eredményt és a diagnosztikai zajt külön
akarjuk tárolni. A shellben a > a stdout-ot
irányítja fájlba, a 2> a stderr-t.
PowerShellben ugyanez a két operátor működik az újabb verziókban, de az
exit code ott a $LASTEXITCODE változóban jelenik meg, nem a
$? boolean értékben.
Minden folyamat egy számmal tér vissza: ez az exit code, más néven
visszatérési kód (exit status). A 0 hagyományosan sikert jelent, a
nullától eltérő hibaosztályt. A konkrét szám a program saját szerződése.
Bashban a $? az utolsó előtérben futott parancs kódját
adja; ezért az értéket azonnal el kell menteni, mielőtt bármely másik
parancs felülírná. Kézi kontroll Linux/macOS Bashban vagy WSL-ben, a
projektgyökérből:
python -c 'import sys; print("adat"); print("hiba", file=sys.stderr)' \
>reports/s006/out.txt 2>reports/s006/err.txt
status=$?
printf 'exit_code=%s\n' "$status"Itt az out.txt egyetlen adat sort
tartalmaz, az err.txt a hiba sort, az
exit_code=0 pedig a sikerjelzést. A Python-kód semmilyen
hibát nem váltott ki; a stderr-re írás önmagában nem rontja
el a folyamat státuszát.
Három különböző jelenséget könnyű összekeverni, mert mind „hibának” látszik:
| Jelenség | Ki állítja le | Látható jel | Kilépőkód |
|---|---|---|---|
| Python-kivétel | a Python interpreter saját kivételkezelője | Traceback ... a stderr-en |
általában 1 |
| OOM (out of memory) | az operációs rendszer memóriakezelője | gyakran nincs traceback, Linuxon Killed |
Linuxon gyakran 137 |
| Külső folyamat hibakódja | a folyamat saját return/exit
utasítása |
a shell $? változója |
programspecifikus |
A kezeletlen Python-kivétel (exception) nyelvi hibacsatorna: a
1/0 kifejezés tracebacket ír, a processz kontrolláltan
leáll, és a shell $? értéke 1 lesz. A
MemoryError is kivétel: a Python megpróbált egy számára túl
nagy objektumot lefoglalni, és erről a saját nyelvi mechanizmusán
keresztül számolt be. Ez még nem jelenti, hogy az egész gép memóriája
elfogyott.
Az OOM ezzel szemben operációs rendszer szintű esemény: ha a processz
a rendelkezésre álló RAM-nál többet kér, a kernel megölheti anélkül,
hogy a Python tracebacket írna. Linuxon a 137-es kilépőkód a 128 + 9
összegből adódik, ahol a 9 a SIGKILL jel sorszáma; ez
gyakori OOM-gyanú, de önmagában nem elégséges bizonyíték. macOS-en más
memórianyomás-kezelés működik, Windowson pedig a jelenség más üzenettel
jelentkezhet.
A külső folyamat hibakódja sem Python-kivétel. Ha egy adateszköz, például egy parancssori program 4-gyel tér vissza, az az adott program szerződése szerinti hiba. Pythonból ezt így lehet lekérdezni, szemléltető jelleggel:
import subprocess
result = subprocess.run(["python", "-c", "import sys; sys.exit(7)"])
print(result.returncode) # elméletileg várt: 7A subprocess.run alapból nem dob Python-kivételt
nemnulla visszatéréskor; a check=True kapcsolóval
kérhetnénk kivételt. A kettő szétválasztása azért fontos, mert a
hibanaplóban más javító lépés következik: kivételnél a kódot, OOM-nál a
memóriaterhelést, külső hibakódnál az adott program dokumentációját kell
vizsgálni.
A processz (folyamat) saját memóriaterületet és erőforrásokat kap; a
thread (szál) ugyanazon processz memóriáján osztozó, könnyebb ütemezési
egység. Egy Jupyter-kernel egy hosszú életű Python-processz; ha két
notebook fut, legalább két külön kernelprocessz dolgozik. Linuxon a
ps -o pid,ppid,rss,etime,comm, macOS-en a
ps -axo ..., Windowson a Get-Process python
segít azonosítani, melyik folyamat mennyi memóriát foglal. A
ps eszköz rss mezőjének szokásos mértékegysége
KiB (1024 bájtos egység), nem bájt; a pontos formátumot az adott
platform dokumentációja szerint kell értelmezni. Az RSS a rezidens
memórialapok nagyságát közelíti, és megosztott lapokat is tartalmazhat:
nem azonos a folyamat kizárólagos fizikai RAM-terhelésével, és nem a
lemezfoglalást méri.
A RAM és a lemez közötti különbség a hibadiagnózis alapja. A fájl
mérete path.stat().st_size értékkel kérdezhető le; ez a
lemezen elfoglalt tartós méret. A processz RAM-használata futás közben
ennél lehet kisebb, ha streamelve dolgozunk, vagy nagyobb, ha a sorokat
Python-listában, string-objectumokként tároljuk. A durva becslés:
ahol $B_{\text{fájl}}$ a CSV byte-mérete,
$N_{\text{sor}}$ a sorok száma,
$C_{\text{sor}}$ pedig egy sor listaelemének becsült
overheadje. 64 bites CPython 3.12 környékén
$C_{\text{sor}}$ nagyságrendileg 40–60 byte, mert egy
listaelem 8 byte-os mutatót és egy legalább 49 byte-os string-objektumot
jelent; ez becslés, nem mérés. A bináris átváltás:
A megszakítás (interrupt) a harmadik hibacsatorna. Az előtérben futó
folyamatot Ctrl+C szakítja meg; Linux/macOS-en ez
SIGINT jelet küld, a Python pedig
KeyboardInterrupt kivétellé alakítja. A
SIGKILL viszont nem kivétel: a processz azonnal megszűnik,
finally blokk sem fut. A Jupyterben a kernel Interrupt
gombja hasonló szerepet tölt be, de a kernel folyamatának tényleges
állapotát hosszú feladat után külön ellenőrizni kell. A háttérben
indított folyamat PID-jét a shell $! változója adja;
Linuxon a kill PID, Windowson a
Stop-Process -Id PID állítja le.
Munkakönyvtár a projektgyökér; platform Linux/macOS Bash vagy WSL.
Első lépésként mentsük el az alábbi ellenőrzőt
scripts/s006_diagnose.py néven:
from __future__ import annotations
import csv
import sys
from pathlib import Path
EXPECTED_HEADER = [
"order_id", "customer_id", "order_ts", "promised_days", "quantity_sum",
"item_rows", "basket_minor", "shipping_fee_minor", "currency", "distance_km",
"priority_flag", "delivery_days", "actual_delivery_at", "label_available_at",
"target_late",
]
MIB = 1024 * 1024
def main(argv: list[str]) -> int:
if len(argv) != 2:
print(f"Használat: {Path(sys.argv[0]).name} BEMENET.csv", file=sys.stderr)
return 2
path = Path(argv[1])
if not path.is_file():
print(f"Nincs ilyen fájl: {path}", file=sys.stderr)
return 1
total_bytes = path.stat().st_size
rows = 0
issues: list[str] = []
seen: set[str] = set()
with path.open("r", encoding="utf-8", newline="") as handle:
reader = csv.reader(handle)
header = next(reader, None)
if header != EXPECTED_HEADER:
print(f"Fejléc eltér: {header}", file=sys.stderr)
return 3
for row in reader:
rows += 1
if len(row) != len(EXPECTED_HEADER):
issues.append(f"sor {reader.line_num}: rossz mezőszám: {len(row)}")
continue
order_id = row[0]
if order_id in seen:
issues.append(f"sor {reader.line_num}: duplikált order_id {order_id}")
seen.add(order_id)
if row[9] == "":
issues.append(f"sor {reader.line_num}: hiányzó distance_km")
try:
if int(row[6]) < 0:
issues.append(f"sor {reader.line_num}: negatív basket_minor {row[6]}")
except ValueError:
issues.append(f"sor {reader.line_num}: nem numerikus basket_minor {row[6]}")
print(f"fajl={path}")
print(f"rows_read={rows}")
print(f"issue_count={len(issues)}")
print(f"total_bytes={total_bytes}")
print(f"average_row_bytes={total_bytes / rows:.1f}")
estimated_full_load = total_bytes + rows * 60
print(f"estimated_full_load_mib={estimated_full_load / MIB:.2f}")
for issue in issues:
print(issue, file=sys.stderr)
return 4 if issues else 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))A script a hibás bemeneten nemcsak leáll, hanem a hibasorokat a
stderr-re írja, az összegzést a stdout-ra, a
nullától eltérő kóddal pedig a shellben automatizálhatóan jelez. A
következő futtatás a reports/s006 mappába gyűjti a
futásnaplót:
mkdir -p reports/s006
python scripts/s006_diagnose.py data/raw/orders_raw.csv \
>reports/s006/bad.out 2>reports/s006/bad.err
status=$?
printf 'exit_code=%s\n' "$status" >reports/s006/bad.log
cat reports/s006/bad.log
cat reports/s006/bad.out
cat reports/s006/bad.errSzemléltető várható kimenet — a byte-számok a saját gépen futó
generate_data.py eredményétől függenek:
exit_code=4
fajl=data/raw/orders_raw.csv
rows_read=2405
issue_count=8
total_bytes=882419
average_row_bytes=366.8
estimated_full_load_mib=0.98
sor 118: duplikált order_id ORD-...
sor 487: hiányzó distance_km
sor 902: hiányzó distance_km
sor 1203: negatív basket_minor -1350
sor 1501: duplikált order_id ORD-...
sor 1704: duplikált order_id ORD-...
sor 1988: duplikált order_id ORD-...
sor 2210: duplikált order_id ORD-...
A tiszta data/raw/orders.csv kontrollként futtatva:
python scripts/s006_diagnose.py data/raw/orders.csv \
>reports/s006/good.out 2>reports/s006/good.err
status=$?
printf 'exit_code=%s\n' "$status"Várható: exit_code=0, rows_read=2400,
issue_count=0, üres good.err. A
PowerShell-munkamenetben ugyanez a szerkezet 2>
átirányítással és $LASTEXITCODE rögzítésével zajlik.
A kézi kontroll a nyers fájl szerződéséből következik: 2405 sor =
2400 alapsor + 5 duplikált sor; a hiányzó distance_km két
sora és a negatív basket_minor egy sora külön sorokon van,
így összesen 8 észlelt sorhiba. A rows_read=2405 és
issue_count=8 páros egzakt invariáns; ha bármelyik eltér, a
fájl vagy a script változott. Az exit_code=4 a script saját
szerződése: 0 tiszta, 1 hiányzó fájl, 2 rossz argumentum, 3
fejléc-eltérés, 4 sorhibák.
A memóriasor becslés, nem mérés. Az
estimated_full_load_mib azt mondja meg, mennyit foglalna a
sorok szövegének és a hozzá tartozó Python stringeknek a listában
tartása; a futó script streamelve olvas, ezért valós RSS-használata
ennél kisebb. Pontos processzmemóriát mérni csak saját gépen, megadott
Python-verzióval, ps-sel vagy platformes eszközzel lehet;
az itt közölt kimenet szemléltető.
Folyamat és mérés. A stdout a strukturált eredmény vagy
emberi kimenet csatornája, a stderr a diagnosztikai
üzeneteké; egyes eszközök eltérő konvenciót követnek, ezért a naplózási
szerződés fontosabb, mint a puszta csatorna. A kilépési kódot a vizsgált
parancs után közvetlenül rögzítsük, különben a következő
cat vagy echo már felülírja a shell
$? értékét.
CPU-kihasználtságból önmagában nem következik, hogy a Python-kód lassú vagy gyors: a méréshez a folyamat futásideje, I/O-tevékenysége, memóriája és a rendelkezésre álló magok száma is kell. A nagyobb CSV vizsgálatakor az adatméret és az olvasási mód legyen a mérési napló része.
Értelmezés. A hibás futtatás után a
bad.log tartalma exit_code=4. Melyik állítás
igaz?
bad.err-ben.stderr-re írta,
és a saját hibakódja 4.Implementálás. Bővítsd a
s006_diagnose.py scriptet egy harmadik, opcionális
paranccsal: ha a felhasználó megadja a
data/raw/customers.csv fájlt, a script töltse be az ismert
customer_id értékek halmazát, és minden olyan rendeléssort
jelentsen hibaként, amelynek customer_id-je nem szerepel
benne. A customers fájl első oszlopa a
customer_id.
Hibadiagnózis. Adott a következő Bash-részlet:
python scripts/s006_diagnose.py data/raw/orders_raw.csv
cat reports/s006/bad.out
echo "status=$?"Miért lehet félrevezető az így kiírt status akkor is, ha
a Python-futtatás 4-gyel tért vissza? Add meg a javított
változatot.
Transzfer megváltozott feltételre. Készíts 130
000 soros szintetikus fájlt data/derived/orders_130k.csv
néven úgy, hogy a data/raw/orders.csv fejlécét és egyetlen
érvényes adatsorát ismétled. Futtasd rajta a scriptet, és értelmezd a
memóriabecslést. Melyik értéknek kell egzaktnak lennie, és melyiknél
fogadható el ±20%-os eltérés?
A helyes válasz b. A 4-es kód a script
alkalmazásszintű hibaszerződése, nem Python-kivétel és nem OOM. A
bad.err soralapú diagnózisokat tartalmaz, nem tracebacket;
a bad.out issue_count=8 értéke pedig
megerősíti, hogy a 4-es a nyolc sorhibához kötött saját
konvenció.
A módosítás lényege:
if len(argv) == 3:
customer_path = Path(argv[2])
raw_lines = customer_path.read_text(encoding="utf-8").splitlines()[1:]
known_customers = {line.split(",")[0].strip() for line in raw_lines}
else:
known_customers = NoneA sorciklusban, a mezőszám-ellenőrzés után:
if known_customers is not None and row[1] not in known_customers:
issues.append(
f"sor {reader.line_num}: ismeretlen customer_id {row[1]}"
)Futtatás:
python scripts/s006_diagnose.py data/raw/orders_raw.csv data/raw/customers.csv \
>reports/s006/extended.out 2>reports/s006/extended.err
status=$?
printf 'exit_code=%s\n' "$status"Az ellenőrző invariáns: rows_read=2405,
issue_count=9, exit_code=4. A nyers fájlban
egyetlen ismeretlen customer_id van, ezért pontosan eggyel
több hiba keletkezik.
$? mindig az utoljára
végrehajtott parancs kódját adja; az echo előtt a
cat reports/s006/bad.out fut le, amely sikeres
fájlmegjelenítéskor 0-t ad. Így a kiírt status=0 a
cat eredményét mutatja, nem a Python folyamatét. A javított
változat a Python-kódot közvetlenül követően menti az értéket:python scripts/s006_diagnose.py data/raw/orders_raw.csv
status=$?
cat reports/s006/bad.out
echo "status=$status"mkdir -p data/derived
python - <<'PY'
from pathlib import Path
raw = Path("data/raw/orders.csv")
lines = raw.read_text(encoding="utf-8").splitlines()
rows = [lines[0]] + [lines[1]] * 129_999
Path("data/derived/orders_130k.csv").write_text(
"\n".join(rows) + "\n", encoding="utf-8"
)
PY
wc -l data/derived/orders_130k.csv
python scripts/s006_diagnose.py data/derived/orders_130k.csv \
>reports/s006/big.out 2>reports/s006/big.err
status=$?
printf 'exit_code=%s\n' "$status"
cat reports/s006/big.outA wc -l eredményének 130 000-nek, a script
rows_read értékének 129 999-nek kell lennie, és
issue_count=0, exit_code=0 várható, mert a
minta sor érvényes. A becsült teljes tárolás nagyságrendileg a 2405
soros fájl ötvenszerese: ha az eredeti becslés 1 MiB körül volt, itt
50–55 MiB várható. Ez a becslés ±20%-os toleranciával értelmezhető, mert
a fájl mérete és a string-overhead közelítése nem platformfüggetlen. Az
exit_code=0 és az issue_count=0 viszont egzakt
invariáns.
Egy futtatás közben a felhasználó azt látja, hogy a Python-processz
CPU-kihasználtsága 3%. Ebből gyakran „a CPU lassú, GPU-ra van szükség”
következtetést vonnak le. Ez hibás: az alacsony CPU-használat ugyanúgy
lehet lemezolvasási várakozás, hálózati késés vagy adatbázis-tranzakciós
blokk. A fenti script 3%-os processzornál is végigfuthat, mert az ideje
nagy részét a fájlrendszer olvasására várja; egy GPU semmivel sem
olvasná gyorsabban a spinning diskről érkező bájtokat. A GPU akkor jön
szóba, ha a szűk keresztmetszet numerikus, párhuzamosítható művelet, és
ezt mérés bizonyítja. Ez a döntés a későbbi profilozási fejezet témája,
nem a pillanatnyi CPU-százalék. A 1.2
adatéletciklus-fegyelme itt is érvényes: a distance_km
hiánya a rendelés időpontjában ismert mező adatminőségi hibája, nem
későn érkező label, ezért javítása nem sérti a predikciós sorrendet. Az
2.1 projektkonvenciót követtük: a nyers adatot
nem módosítottuk, a futásnaplók a reports/s006
származtatott mappába kerültek. Önellenőrzés: igaz-e, hogy a
stderr-re írás mindig nemnulla exit code-ot okoz? Nem: a
print(file=sys.stderr) önmagában nem állítja a hibakódot.
Igaz-e, hogy a 137 minden platformon OOM-ot jelent? Nem:
Linuxon SIGKILL-gyanú, más rendszeren más konvenció él.
A futó adatfolyamatnak három hibacsatornája van: a
stdout az adatszerű kimeneté, a stderr a
diagnosztikáé, az exit code pedig a processz automatikusan ellenőrizhető
összegzéséé. A Python-kivétel nyelvi traceback, az OOM operációs
rendszer szintű megállítás, a külső hibakód a program saját szerződése.
A memóriabecslés a fájlméretből és a soronkénti overheadből
nagyságrendet ad; a pontos RAM-mérést platformfüggő eszközzel kell
elvégezni. A napló rögzítése után a következő lépésben a
Git-verziókezelést kapcsoljuk a projekthez, hogy a
scripts/s006_diagnose.py, a reports/s006
kimenetek és a nyers adat változásai nyomon követhetők legyenek.