A Northstar labor mappája több parancsot is futtathatóvá tesz: a
lab/generate_data.py előállítja a tanulóadatokat, a
lab/quickcheck.py pedig ellenőrzi a szerződött fájlokat. Az
első kellemetlen tapasztalat általában nem az adattal, hanem az
útvonallal van: ugyanaz a parancs működik a repository gyökeréből, de
megtörik, ha a lab mappából indítjuk, vagy éppen
fordítva.
A feladat most az, hogy ugyanazt a labparancsot két különböző
munkakönyvtárból is ugyanazzal az eredménnyel futtassuk, majd a
véletlenszerű CWD-függést egy rögzített projektgyökér-szerződéssel
szüntessük meg. A megfigyelési egység itt egy teljes parancsinvokáció a
hozzá tartozó munkakönyvtárral, a keletkező fájlokkal és az ellenőrző
hash-szel. A siker feltétele az, hogy a két különböző helyről indított
futtatás azonos fájlokat, azonos tartalommal hozzon létre; a
hash-egyezésben nincs tolerancia, mert a quickcheck.py
jellegű ellenőrzés bitpontosan hasonlítja össze a rögzített
kontrollértékeket.
Előzetes becslés: ha egy script data/raw stílusú relatív
utat használ, és a generátor a CWD-be írja a kimenetét, akkor a
gyökérből indított python lab/generate_data.py a projekt
gyökerébe teszi a fájlokat, a
cd lab && python generate_data.py pedig a
lab mappába. Az egyik futás biztosan nem oda ír, ahol a
quickcheck.py keresi a fájljait, vagy a manifest hash
eltér. A hiba nem az adat, hanem a futási hely hallgatólagos
feltételezése.
Az aktuális munkakönyvtár (current working
directory, CWD) egy folyamat szintű tulajdonság. Amikor egy
terminálban cd projektek/northstar parancsot adunk, a héj
munkakönyvtára megváltozik, és minden ezután indított alárendelt
folyamat, például a Python interpreter, ezt a munkakönyvtárat örökli.
Bashban a pwd, PowerShellben a Get-Location,
Pythonban a Path.cwd() mutatja. A CWD nem a script
forrásfájljának helye, nem a notebook mappája és nem a repository
gyökere automatikusan; csak az a könyvtár, ahonnan a folyamat
indult.
A relatív útvonal data/raw/orders_raw.csv mindig a
CWD-hez képest értelmezett. Ezért ugyanaz a karaktersorozat két helyről
indítva egészen más fájlra mutat. Linuxon, macOS-en és WSL-ben a gyökér
és a mappák hierarchiája /-rel kötött, Windowson
\-rel; a pathlib éppen ezért platformfüggetlen
Path("data") / "raw" felépítést javasol a kézzel írt
data\raw helyett.
pathlib: a
script helyéből számított útA Python standard könyvtárának pathlib modulja
objektumként kezeli az útvonalakat. A legfontosabbak:
Path(__file__) a futó Python-fájl helye. Fontos: nem a
CWD, hanem a script tényleges elérési útja..resolve() abszolút úttá alakít és feloldja a
szimbolikus linkeket..parent a szülő könyvtár, .parents[i] az
i-edik felmenő; a .parents[0] megegyezik a
.parent-tel..mkdir(parents=True, exist_ok=True) létrehozza a
hiányzó könyvtárláncot..write_text(...) és .read_text(...)
szövegfájlokhoz; explicit encoding="utf-8" ajánlott, mert
az alapértelmezett kódolás a locale-tól függhet..stat().st_size a fájl bájtban mért mérete.Ha egy script a s005_demo/scripts/make_files.py helyen
fekszik, akkor a projektgyökér így rögzíthető:
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] # s005_demo/scripts szülője: s005_demoA módszer érvényességi korlátja, hogy a __file__ csak
normál, forrásfájlként futtatott szkriptben értelmezhető.
Jupyter-notebookban nincs __file__; interaktív
interpreterben, becsomagolt vagy „fagyasztott” futtatható állományban
nem feltétlenül valós fájlrendszeri útvonalra mutat. Az alfejezet
módszere tehát parancssori scriptekre és modulokra szól, a notebookokra
a későbbi 3.4 kernelregisztrációs munkamenet ad
teljes megoldást.
data/raw és
data/derived: bemenet és felülírható kimenetA kutatási projekt akkor marad hordozható, ha az olvasó szeme meg tudja különböztetni az eredeti bemenetet az előállított kimenettől. A szokásos szerződés:
data/raw/ – a projektbe érkező, nem módosítandó
forrásfájlok. A futtató kód ezt csak olvassa. A Northstar laborban a
lab/orders_raw.csv pont ilyen szerepű: szándékosan hibás
nyers másolat, tisztítási bemenet.data/derived/ – a kódból előállított, reprodukálhatóan
felülírható kimenet. Itt van a tisztított orders.csv és a
futási ellenőrzéshez tartozó manifest.json szellemi
párja.A kezdőlabor fájlszinten választja szét a nyers és tiszta adatot; nagyobb projektben a mappaszerkezet is ezt a szerződést tükrözi. A lényeg nem a mappa neve, hanem a felülírhatósági szabály: a raw állománynak nem szabad futás melléktermékeként készülnie, a derived állományt pedig bármikor el lehet dobni, mert a generáló utasítás adott. A kimeneti út (output path) tehát nem lehet a CWD rejtett mellékhatása, hanem a projektgyökérből levezetett, dokumentált útvonal.
Ugyanaz a látható szöveg eltérő bájtokat jelenthet, és a manifest
hash bájtokra számol, nem látszatra. A árvíz öt karakter,
de UTF-8 kódolásban hét bájt, mert az á és az
í két-két bájtot foglal: C3 A1, illetve
C3 AD. A karakterek és bájtok megkülönböztetése itt
konkrét: a Python len("árvíz") értéke 5, a
fájlbeli hossz viszont 7 bájt.
A sorvég ugyanilyen láthatatlan veszélyforrás. Linuxon és macOS-en a
sorvég \n (0A), Windowson a hagyományos CRLF
\r\n (0D 0A). A Python open()
íráskor newline=None esetén Windowson CRLF-re fordíthat; a
newline="\n" argumentum explicit LF-et kényszerít. A
Path.write_text(..., newline="\n") a Python 3.10-től érhető
el, a labor alapjaként használt Python 3.12-ben tehát rendelkezésre áll.
A BOM (EF BB BF) sem látható, de megváltoztatja a hash-t; a
fájlt a xxd -l 16 kezdősora leplezi le.
A sorvéget verziókezelt projektekben a .gitattributes
rögzítheti, például:
*.py text eol=lf
*.csv text eol=lf
*.sh text eol=lf
*.md text eol=lf
Így a CSV-k tartalma platformon át hordozható, és a manifest hash-ek is egyezhetnek, ha a generátor determinisztikus.
Hozzunk létre egy s005_demo nevű miniprojektet a
következő fastruktúrával:
s005_demo/
scripts/make_files.py
data/raw/ # a futás hozza létre
data/derived/ # a futás hozza létre
A scripts/make_files.py tartalma:
from pathlib import Path
# A script a s005_demo/scripts/ mappában van, ezért a projektgyökér a szülő.
ROOT = Path(__file__).resolve().parents[1]
RAW = ROOT / "data" / "raw"
DERIVED = ROOT / "data" / "derived"
RAW.mkdir(parents=True, exist_ok=True)
DERIVED.mkdir(parents=True, exist_ok=True)
source = RAW / "note.txt"
source.write_text("első sor: árvíz €\n", encoding="utf-8", newline="\n")
target = DERIVED / "note_upper.txt"
target.write_text(source.read_text(encoding="utf-8").upper(),
encoding="utf-8", newline="\n")
print(f"ROOT={ROOT}")
print(f"source={source.relative_to(ROOT)} size={source.stat().st_size}B")
print(f"target={target.relative_to(ROOT)} size={target.stat().st_size}B")Futtassuk ugyanazt a scriptet két különböző munkakönyvtárból. Linux/macOS bash munkamenetben:
# Platform: Linux/macOS. Munkakönyvtár először maga a projektgyökér.
cd /home/user/projects/s005_demo
python scripts/make_files.py
# Ezután a szülőből ugyanaz a script:
cd /home/user/projects
python s005_demo/scripts/make_files.pyAz elméletileg várt, szemléltető kimenet, ha a felhasználó útja
/home/user/projects:
ROOT=/home/user/projects/s005_demo
source=data/raw/note.txt size=23B
target=data/derived/note_upper.txt size=23B
A második futás pontosan ugyanazt a ROOT értéket
nyomtatja, mert az útvonalat a __file__ helyéből
származtattuk, nem a CWD-ből. A relatív
scripts/make_files.py hívási útvonal eltérhet, a
.resolve() akkor is ugyanarra az abszolút fájlra mutat.
A script három dolgot ellenőrizhetővé tesz. Először, mindkét futás
ugyanazt az abszolút ROOT-ot írja, tehát a kimeneti fájlok
ugyanoda kerülnek. Másodszor, a source és a
target mérete egyaránt 23 B: az eredeti „első
sor: árvíz €” szöveg 23 bájt UTF-8-ban, és a nagybetűs „ELSŐ SOR: ÁRVÍZ
€” is 23 bájt, mert bár a karakterek közül négy több-bájtos, a kis- és
nagybetűs pár ugyanannyi bájtot foglal. A € három bájt
(E2 82 AC), a \n egy bájt (0A), a
többi ékezetes betű kettő-kettő. Harmadszor, a kiíratás relatív
útvonalat használ a relative_to(ROOT) hívással: a naplóban
data/raw/note.txt szerepel, nem a gépspecifikus abszolút
út, így a két gépen készült naplók csak akkor térhetnek el, ha valóban
eltérő a tartalom.
Bitpontos hash-ellenőrzést is futtathatunk a s005_demo
gyökérből:
# Platform: Linux/macOS. Munkakönyvtár: /home/user/projects/s005_demo
python - <<'PY'
from pathlib import Path
import hashlib
for p in sorted(Path("data").rglob("*.txt")):
digest = hashlib.sha256(p.read_bytes()).hexdigest()
print(p, digest)
PYA két futás után a hash azonos; az ellenőrzés itt bitpontos, tolerancia nélküli. Ha a fájlt CRLF-re mentenék, a hash azonnal megváltozna, pedig a képernyőn ugyanaz a szöveg látszana.
Hordozható útvonal-szerződés. A munkakönyvtár (CWD), a script helye
és a projekt gyökere eltérő fogalmak. A Path(__file__) a
modul helyéből indul ki, ezért csak akkor jó a parents[1],
ha a dokumentált könyvtárstruktúra valóban fennáll. Telepített csomag
esetén az adatfájlok feloldását inkább explicit konfigurációval vagy
csomagerőforrással végezzük.
Platformellenőrzés: az útvonalillesztéshez
pathlib-ot használjunk, ne kézzel fűzzünk \
vagy / jeleket. Tesztelendő a szóközt tartalmazó projektút,
az eltérő CWD és a nem létező bemenet: utóbbinál legyen értelmes
hibaüzenet, ne néma új fájl létrehozása.
1. feladat (értelmezés). Adott a következő fastruktúra:
projekt/
scripts/run.py
data/input.csv
A scripts/run.py mindössze ennyit tesz:
from pathlib import Path
print(Path.cwd())
print(Path(__file__).resolve())
print(Path("data/input.csv").resolve())Futtatjuk először
cd projekt && python scripts/run.py, majd
cd projekt/scripts && python run.py paranccsal.
Mindkét futásnál add meg a három sor várt kimenetét, és magyarázd meg,
miért változik a harmadik sor, pedig a script ugyanaz. Melyik futásban
létezik a data/input.csv fájl?
2. feladat (implementálás). A Northstar repository
gyökerébe hozz létre run_lab.sh wrapper szkriptet. Adott
input a következő:
northstar/
lab/generate_data.py
lab/quickcheck.py
A wrapper a saját fájlhelyéből számítsa ki a projektgyökeret, váltson
a lab mappába, majd futtassa a
generate_data.py és quickcheck.py parancsokat.
Futtasd a wrapper szkriptet a gyökérből, a lab mappából és
egy teljesen más munkakönyvtárból is. Mi a siker feltétele, és miért nem
elég a python generate_data.py közvetlen hívása a
gyökérből?
3. feladat (hibadiagnózis). A
/home/user/northstar/notebooks/check.ipynb notebook első
cellája:
from pathlib import Path
raw = Path("lab/orders_raw.csv")
print(raw.exists(), raw.resolve())A Jupyter Labot a /home/user könyvtárból indítottuk,
majd a notebookot a fájlkezelőben a northstar/notebooks
mappából nyitottuk meg. A cella False értéket és
/home/user/lab/orders_raw.csv útvonalat ír. Miért? Adj két
javítást, amelyek egyike a Jupyter indítási szerződését rögzíti, a
másika védő asserttel leáll, ha a feltétel nem teljesül! Miért
nem jó megoldás egy
os.chdir("/home/user/northstar") cellát beszúrni a notebook
elejére?
4. feladat (transzfer, PowerShell). Írd át a
run_lab.sh-t run_lab.ps1 formába Windows
PowerShellre. A PowerShell script a $PSScriptRoot változót
használja, ne az aktuális könyvtárat! A generált CSV fájlok sorvége
maradjon LF, függetlenül attól, hogy a .ps1 fájlt CRLF-fel
szerkesztettük. Mit ellenőriz a manifest hash? Mi történik, ha egy
Windows-szerkesztő a generált orders.csv fájlt CRLF-re
normalizálja?
1. feladat megoldása. Az első futásnál a CWD
projekt, a __file__
projekt/scripts/run.py, a relatív
data/input.csv pedig projekt/data/input.csv.
Várt kimenet:
/home/user/projektek/projekt
/home/user/projektek/projekt/scripts/run.py
/home/user/projektek/projekt/data/input.csv
A második futásnál a CWD projekt/scripts, a
__file__ ugyanaz a script, de a relatív út a CWD-hez képest
oldódik fel, tehát projekt/scripts/data/input.csv. A
második futásban a fájl általában nem létezik; a relatív útvonal a
munkakönyvtártól függ, nem a script forrásfájljának helyétől. Pontosan
ezért köti a projektgyökér-szkript a ROOT-ot a
__file__-hoz.
2. feladat megoldása. A run_lab.sh
tartalma:
#!/usr/bin/env bash
set -euo pipefail
# A szkript saját könyvtárából számítjuk a projektgyökeret:
# a CWD nem befolyásolhatja a generálás helyét.
THIS_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LAB_DIR="$THIS_DIR/lab"
cd "$LAB_DIR"
python generate_data.py
python quickcheck.pyFuttatás három munkakönyvtárból:
# Linux/macOS bash. Először a repository gyökere:
cd /home/user/northstar
./run_lab.sh
# Ugyanaz a szkript a lab mappából:
cd lab
../run_lab.sh
# Teljesen más munkakönyvtárból:
cd /tmp
/home/user/northstar/run_lab.shA siker feltétele az, hogy a lab/generate_data.py által
írt fájlok mindhárom esetben a lab mappába kerüljenek, és a
lab/quickcheck.py ellenőrző hash-e egyezzen a
lab/manifest.json rögzített kontrollértékével. A közvetlen
python generate_data.py hívás azért nem elég a gyökérből,
mert a gyökérben nincs ilyen fájl: lab/generate_data.py
lenne a hívandó, ám ha a generátor a CWD-t használja a kimenethez, akkor
a fájlok a gyökérbe kerülnének, ahol a quickcheck.py nem
találja őket a szerződött helyen. A wrapper mindig a lab
mappába cd-zik, tehát a generátor CWD-je
determinisztikus.
A set -euo pipefail azt jelenti: hiba esetén a script
azonnal leáll, nem hagyja figyelmen kívül a hibás parancsot. Az
unset változó (-u) is hibának számít; ez
megakadályozza az elgépelt útvonalak csendes továbbvitelét.
3. feladat megoldása. A Jupyter kernel CWD-je az
indítási könyvtár, nem a notebook mappája. A notebookot a böngészőben a
northstar/notebooks mappából nyitottuk meg, de a kernel
/home/user-ből indult, ezért a
Path("lab/orders_raw.csv") a
/home/user/lab/orders_raw.csv helyre mutat, ami nem
létezik. A notebook helye nem változtatja meg a kernel CWD-jét; a kettő
eltérhet, és ez a fejezet gyakori tévedése.
Első javítás: rögzítsd az indítási szerződést, és indítsd a Jupyter Labot a projektgyökérből:
# Linux/macOS bash, a northstar gyökérből:
cd /home/user/northstar
jupyter labEkkor a kernel CWD-je a projektgyökér, és a cellában a relatív
lab/orders_raw.csv elérési út helyesen oldódik fel.
Második javítás: védő assert a notebook első cellájában:
from pathlib import Path
PROJECT_ROOT = Path.cwd()
REQUIRED = PROJECT_ROOT / "lab" / "orders_raw.csv"
assert REQUIRED.exists(), f"A kernel CWD nem a Northstar gyökér: {PROJECT_ROOT}"
RAW = REQUIREDEz még mindig feltételezi, hogy a Jupytert a projektgyökérből
indították, de a feltétel nem hallgatólagos többé: ha megszegik, a
notebook azonnal, értelmes üzenettel áll le. Az
os.chdir("/home/user/northstar") rossz megoldás, mert a
cellába gépspecifikus abszolút utat éget; más gépen megtörik, és a cella
újrafuttatása állapotot módosít. A teljes, hordozható kernelindító
megoldást a 3.4 alfejezet építi fel.
4. feladat megoldása. A run_lab.ps1
tartalma:
# Futtatás: powershell -ExecutionPolicy Bypass -File .\run_lab.ps1
$ErrorActionPreference = "Stop"
$Root = $PSScriptRoot
$Lab = Join-Path $Root "lab"
Set-Location $Lab
python generate_data.py
if ($LASTEXITCODE -ne 0) {
throw "generate_data.py failed with exit code $LASTEXITCODE"
}
python quickcheck.py
if ($LASTEXITCODE -ne 0) {
throw "quickcheck.py failed with exit code $LASTEXITCODE"
}A $PSScriptRoot a script mappájának abszolút útja, így a
hívó PowerShell munkakönyvtára nem befolyásolja a futást. A
$LASTEXITCODE a külső python program
visszatérési értéke; PowerShellben a Python hibáját nem dobja
automatikusan a shell, ezért kell explicit ellenőrzés.
A generált CSV fájlok sorvége attól függetlenül LF marad, hogy a
.ps1 forrást CRLF-fel szerkesztettük, mert a CSV-t a Python
generate_data.py írja. Ha a generátor a szöveges íráskor
explicit newline="\n" beállítást használ, akkor a fájlbeli
bájtok közömbösek a Windows szerkesztő beállításaira. A manifest hash a
CSV fájlok tényleges bájtjaira számol; ha egy Windows-szerkesztő egy
meglévő orders.csv-t CRLF-re normalizál, a hash
megváltozik, és a quickcheck.py eltérést jelez, jóllehet a
látható szöveg ugyanaz. Ilyenkor a fájlt újra kell generálni, nem a
hash-t módosítani; a .gitattributes eol=lf
szabálya pedig megelőzi, hogy a verziókezelés maga normalizálja eltérően
a fájlt.
A leggyakoribb tévedés a notebook helyének és a kernel
munkakönyvtárának összekeverése. Képzeljük el, hogy a
notebooks/check.ipynb mellé tesszük a data/raw
mappát, és a notebookban Path("data/raw/orders_raw.csv")-t
írunk. Ha a Jupyter Labot a notebooks mappából indítjuk, a
kernel CWD-je a notebooks, tehát a fájl megtalálható. Ha
viszont a Jupyter Labot a home könyvtárból vagy a projekt gyökeréből
indítjuk, a kernel ugyanazt a notebookot futtatja, de a relatív út más
helyre mutat; a notebook mappájának közelsége nem érdekes a kernel
számára.
A 1.1-ben láttuk, hogy a nevező hallgatólagos megválasztása csendesen tönkreteszi az arányszámítást. Itt a CWD hallgatólagos megválasztása teszi ugyanezt a fájlműveletekkel: a script fut, a napló készül, csak éppen nem ott és nem arról a fájlról, amelyet a szerződés állít. A diagnózis mindkét esetben ugyanaz: a láthatatlan feltételt explicitté kell tenni a futás elején.
Tedd fel magadnak a következő kérdéseket, mielőtt egy scriptet megosztasz:
pwd vagy Path.cwd() paranccsal?__file__-ból származik, vagy a
CWD-től függ?Ha valamelyik nemre változik, előbb a fájlkezelési, nem az adatmodell hibájában keressük a bajt.
A hordozható projekt a munkakönyvtártól független projektgyökérből
dolgozik. A Path(__file__).resolve() a script tényleges
helyéből vezeti le a gyökeret, így ugyanaz a parancs két helyről indítva
is ugyanoda ír. A data/raw bemenet és a
data/derived kimenet elkülönítése védi az eredeti fájlokat;
az explicit UTF-8 és LF beállítás pedig a manifest hash
platformfüggetlenségét biztosítja. Jupyterben a kernel CWD-je nem a
notebook mappája, ezért azt vagy indítási szerződéssel, vagy védő
asserttel kell rögzíteni. A következő alfejezetben a futó folyamatokat
vizsgáljuk: hibakimenetet, visszatérési kódot, memóriahasználatot és a
háttérben futó Python-folyamatok azonosítását.