2.4 Kis tudományos szoftverprojekt

2.4.1 Cél, előismeret és induló kérdés

A 2.3. Izolált környezet, notebook és verzió egységben rögzítettük, melyik interpreter, kernel és lockolt csomagprofil futtatja a kódot. Ebben az alfejezetben a notebookban kipróbált cellát valódi, tesztelt szoftverprojektté alakítjuk. A cél nem az, hogy „szebb” kódot írjunk, hanem hogy a kísérlet bemenete, kimenete, konfigurációja és ellenőrzése együtt legyen újrafuttatható. Induló kérdés: mikor mondhatjuk, hogy a notebookbeli függvény és a modulba emelt változat ugyanazt a matematikai leképezést állítja elő, és mit tud ebből a teszt ténylegesen ellenőrizni?

Előismeret: a 2.3. Izolált környezet, notebook és verzió környezeti szerződése. Visszahívás az 1.4. Tanulási diagnózis és kísérleti napló egységből: a seed a generátor kezdőállapota, a futásazonosító a napló kulcsa; a kettő nem cserélhető fel.

2.4.2 Definíciók és intuíció

Tiszta függvény. Egy Python-függvény akkor tiszta, ha referenciálisan átlátszó és nincs megfigyelhető mellékhatása:

  1. a visszatérési érték csak az argumentumoktól függ, ezért minden hívása helyettesíthető a visszaadott értékkel anélkül, hogy a program viselkedése megváltozna (ugyanazon argumentumokra mindig ugyanazt adja, és nem olvas rejtett állapotot: globális változót, fájlt, órát vagy implicit véletlengenerátort);
  2. nincs kívülről megfigyelhető mellékhatása: nem módosítja az argumentumait, nem ír fájlt, nem változtat globális vagy modulállapotot.

A tisztaság a kód szerkezeti tulajdonsága, és független attól, hogy a függvény helyes-e. A helyesség külön állítás: ha a matematikai leképezés f:𝒟→𝒞f: \mathcal D \to \mathcal C, akkor a Python-implementáció FF helyes, ha F(x)=f(x)F(x)=f(x) minden x∈𝒟x\in\mathcal D-re (lebegőpontos számításnál egy rögzített toleranciával). Egy teszt ennek a helyességnek csak véges sok esetét ellenőrzi; a tisztaság viszont azt biztosítja, hogy a tesztben látott eredmény nem függ a processz korábbi állapotától, ezért megismételhető. A fájlírás ezzel szemben eljárás, nem tiszta függvény.

Modul. Olyan Python-fájl, amely importálható névtérként szolgál. A notebookcella kódja modulba emelve függetleníthető a cellák végrehajtási sorrendjétől.

CLI. Parancssori interfész, amely a bemenetet, a kimenetet és az opciókat argumentumként kapja. Ezzel a konfiguráció nem a kódba égetett konstans, hanem a parancs része.

Manifest, futási jegyzék. A futás bemenetének, paramétereinek, környezetének és kimeneti lenyomatának géppel ellenőrizhető rögzítése. A GlyphLab manifest.json-ja ugyanezt az elvet követi: a fájlhash és a splitdarabszám együttesen azonosítja az adatállapotot.

Tesztfixture. Előre, determinisztikusan előállított bemenet és hozzá tartozó elvárt tulajdonság. A fixture nem „példa, ami véletlenül jó”, hanem a teszt szerződésének rögzített inputja.

Lokális artefaktum. A futtatás által létrehozott, nem forráskód jellegű fájl: modellkimenet, napló, generált manifest. A verziókezelőből kizárjuk, mert a futás eredménye, nem a projekt definíciója.

Git. Verziókezelő, amely a projekt forrásállapotát rögzíti. A commit a kód és a fixture időbeli kontrollja; a futás reprodukálásához szükség van még a környezeti lockra és a manifestre.

2.4.3 A refaktor által őrzött invariáns

Állítás (refaktor-egyezés, gyenge determinisztikus lemma). Legyen F0F_0 a notebookban definiált függvény, F1F_1 a modulba emelt függvény. Tegyük fel, hogy

Ekkor bármely xx bemenetre F0(x)=F1(x)F_0(x)=F_1(x).

Indoklás. A feltételek mellett a program determinisztikus: minden utasítás ugyanazt az átmenetet hajtja végre, függetlenül attól, hogy a kód fizikailag notebookcellában vagy modulban van-e. A szekvenciális végrehajtás definíciója szerint azonos kezdőállapotból azonos közbülső állapotok és azonos végeredmény adódik. Ez nyelvi szemantikai érvelés, nem mély matematikai tétel; az állítás ereje a feltételek explicitségében van. A teljes tartományra vonatkozó egyenlőséget ez az érvelés adja, a unit teszt önmagában csak véges mintán ellenőriz.

Mit nem ellenőrizhet a unit teszt? A teszt csak a fixture-ökre vonatkozik; nem bizonyítja a le nem fedett bemenetek viselkedését. Nem dönti el, hogy a függvény a tudományos konvenciónak megfelelő mennyiséget számítja-e (például populációs vagy mintabeli szórást). Nem ad platformok közötti bitazonossági garanciát: az azonos seed sem ígér azonos lebegőpontos eredményt más builden, ahogy a [D02] dokumentáció is rögzíti. Végül a zöld teszt nem helyettesíti a modellértékelés helyességét.

Ellenpélda a feltétel szükségességére. Ha a notebookcella a törzsén kívül definiált, implicit rng = np.random.default_rng() objektumot használ, a modulba emelt változat más processzben más seed nélkül más kimenetet adhat. A cella tehát nem volt tiszta; a refaktornak a generátort argumentummá kell tennie, különben a lemma 1. feltétele sérül.

2.4.4 Kézi példa a standardizáló függvényre

A labor egy oszloponkénti standardizáló függvényt emel ki. Legyen X∈ℝN×dX\in\mathbb R^{N\times d}; a jj oszlopra

μj=1N∑i=1NXij,σj2=1N∑i=1N(Xij−μj)2, \mu_j=\frac1N\sum_{i=1}^{N}X_{ij},\qquad \sigma_j^2=\frac1N\sum_{i=1}^{N}(X_{ij}-\mu_j)^2,

és egy rögzített ε≥0\varepsilon\ge0 véges küszöbbel a kimenet

Yij={Xij−μjσj,ha σj>ε,0,ha σj≤ε. Y_{ij}=\begin{cases} \frac{X_{ij}-\mu_j}{\sigma_j}, & \text{ha } \sigma_j>\varepsilon,\\ 0, & \text{ha } \sigma_j\le\varepsilon. \end{cases}

Ezt nevezzük ε\varepsilon-küszöbös populációs standardizálásnak. Az ε=0\varepsilon=0 eset a tiszta matematikai operátor, amely csak a valóban konstans (σj=0\sigma_j=0) oszlopot nullázza. Az ε>0\varepsilon>0 küszöb skálafüggő degenerációt vezet be: minden olyan oszlop nullává válik, amelynek szórása ε\varepsilon alatti, akkor is, ha nem konstans. Például az X=(0,10−13,2⋅10−13)⊤X=(0,\;10^{-13},\;2\cdot10^{-13})^\top oszlopra σ≈8,16⋅10−14\sigma\approx8{,}16\cdot10^{-14}; ε=0\varepsilon=0 mellett a kimenet (−1,2247;0;1,2247)(-1{,}2247;\ 0;\ 1{,}2247), a labor alapértelmezett ε=10−12\varepsilon=10^{-12} küszöbével viszont nullavektor. A küszöb tehát a bemenet mértékegységéhez kötött döntés, nem numerikus apróság.

Kézi ellenőrzés három megfigyeléssel, két oszloppal:

X=(032541),N=3,d=2. X=\begin{pmatrix}0&3\\2&5\\4&1\end{pmatrix},\quad N=3,\ d=2.

Az első oszlop: μ1=(0+2+4)/3=2\mu_1=(0+2+4)/3=2, a központozott vektor (−2,0,2)(-2,0,2), variancia (4+0+4)/3=8/3(4+0+4)/3=8/3, szórás σ1=8/3\sigma_1=\sqrt{8/3}. A standardizált oszlop (−2,0,2)/8/3(-2,0,2)/\sqrt{8/3}; átlaga 00, populációs varianciája pedig a négyzetes átlag:

13[(−28/3)2+02+(28/3)2]=13⋅88/3=1. \frac{1}{3}\left[\left(\frac{-2}{\sqrt{8/3}}\right)^2+0^2+\left(\frac{2}{\sqrt{8/3}}\right)^2\right] =\frac{1}{3}\cdot\frac{8}{8/3}=1.

A második oszlopra μ2=3\mu_2=3, a központozott vektor (0,2,−2)(0,2,-2), ugyanaz a variancia, tehát az eredmény oszloponként ugyanazt a standardizált alakot adja. Egy konstans harmadik oszlop, például (5,5,5)⊤(5,5,5)^\top, σ=0\sigma=0 miatt a (0,0,0)⊤(0,0,0)^\top konvenciót kapja.

2.4.5 Konkrét labor: notebookból projekt

A labor kiindulópontja a 2.3. Izolált környezet, notebook és verzió notebook egy cellája, amely a fenti standardizálást keveri egy ad hoc kiírással. A feladat a tiszta számítás kiemelése, majd a projekt felépítése.

A projektstruktúra:

Fájl Szerep Commit? Tartalom
glyphstats/normalize.py tiszta függvény igen standardizálás
glyphstats/cli.py parancssori belépés, manifest igen argparse + hash
tests/test_normalize.py teszt igen fixture-ellenőrzés
data/input.npy fixture igen determinisztikus bemenet
runs/ lokális artefaktum nem kimenet és jegyzék

A normalize.py:

"""Oszloponkénti standardizálás az n008 laborhoz."""
from __future__ import annotations

import numpy as np


def normalize_columns(x: np.ndarray, eps: float = 1e-12) -> np.ndarray:
    """Standardizálja az (N, d) mátrix oszlopait, eps-küszöbbel.

    A szórás konvenció: populációs, 1/N normalizálással.
    Ha egy oszlop szórása <= eps, a kimenet 0 (eps=0: csak a konstans oszlop).
    """
    if x.ndim != 2:
        raise ValueError("x.shape-nak (N, d) kétdimenziósnak kell lennie")
    if not (np.isfinite(eps) and eps >= 0):
        raise ValueError("eps-nek véges, nemnegatív számnak kell lennie")
    centered = x - x.mean(axis=0, keepdims=True)
    var = np.mean(centered * centered, axis=0, keepdims=True)
    sd = np.sqrt(var)
    denom = np.where(sd > eps, sd, 1.0)
    out = centered / denom
    out = np.where(sd > eps, out, 0.0)
    return np.ascontiguousarray(out)

A tools/make_fixture.py determinisztikus fixture-t állít elő:

"""Az n008 tesztfixture generátora: seedelt, ismételhető."""
from __future__ import annotations

from pathlib import Path

import numpy as np


def main() -> None:
    out_dir = Path("data")
    out_dir.mkdir(exist_ok=True)
    rng = np.random.default_rng(20261020)
    x = rng.normal(loc=2.0, scale=4.0, size=(10, 3))
    x[:, 2] = 5.0  # konstans oszlop a degenerált esethez
    np.save(out_dir / "input.npy", x.astype(np.float64))


if __name__ == "__main__":
    main()

A cli.py explicit bemenetet, kimenetet és manifestet kezel:

"""Parancssori belépőpont és futási jegyzék."""
from __future__ import annotations

import argparse
import hashlib
import json
import platform
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path

import numpy as np

from .normalize import normalize_columns


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(65536), b""):
            digest.update(chunk)
    return digest.hexdigest()


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="GlyphStats normalizáló CLI")
    parser.add_argument("--input", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--manifest", type=Path, default=None)
    parser.add_argument("--eps", type=float, default=1e-12)
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    input_path = args.input.expanduser().resolve()
    output_path = args.output.expanduser().resolve()
    output_path.parent.mkdir(parents=True, exist_ok=True)

    # Csak saját fixture-t töltünk; allow_pickle=False biztonsági okból.
    x = np.load(input_path, allow_pickle=False).astype(np.float64, copy=False)
    y = normalize_columns(x, eps=args.eps)
    np.save(output_path, y)

    if args.manifest is not None:
        record = {
            "run_id": uuid.uuid4().hex,
            "created_utc": datetime.now(timezone.utc).isoformat(),
            "input_file": input_path.name,
            "input_sha256": sha256_file(input_path),
            "output_file": output_path.name,
            "output_sha256": sha256_file(output_path),
            "params": {"eps": args.eps, "input_dtype": "float64"},
            "python": sys.version.split()[0],
            "numpy": np.__version__,
            "machine": platform.platform(),
            "normalization": "population_std",
        }
        manifest_path = args.manifest.expanduser().resolve()
        manifest_path.parent.mkdir(parents=True, exist_ok=True)
        manifest_path.write_text(json.dumps(record, indent=2), encoding="utf-8")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

A teszt tests/test_normalize.py:

"""Az n008 labor szerződését rögzítő unit tesztek."""
from __future__ import annotations

from pathlib import Path

import numpy as np
import pytest

from glyphstats.normalize import normalize_columns


@pytest.fixture()
def input_array() -> np.ndarray:
    path = Path(__file__).resolve().parent.parent / "data" / "input.npy"
    return np.load(path, allow_pickle=False)


def test_output_shape_matches_input(input_array: np.ndarray) -> None:
    y = normalize_columns(input_array)
    assert y.shape == input_array.shape
    assert y.dtype == np.float64


def test_normalized_columns_are_standardized(input_array: np.ndarray) -> None:
    y = normalize_columns(input_array)
    expected_std = np.array([1.0, 1.0, 0.0])
    np.testing.assert_allclose(y.mean(axis=0), np.zeros(3), atol=1e-12)
    np.testing.assert_allclose(y.std(axis=0), expected_std, atol=1e-12)


def test_zero_variance_columns_are_zero(input_array: np.ndarray) -> None:
    y = normalize_columns(input_array)
    np.testing.assert_array_equal(y[:, 2], np.zeros(input_array.shape[0]))


def test_eps_threshold_is_scale_dependent() -> None:
    x = np.array([[0.0], [1e-13], [2e-13]])
    np.testing.assert_array_equal(normalize_columns(x), np.zeros((3, 1)))   # eps=1e-12
    np.testing.assert_allclose(normalize_columns(x, eps=0.0).ravel(),
                               [-np.sqrt(1.5), 0.0, np.sqrt(1.5)], rtol=1e-9, atol=1e-12)
    with pytest.raises(ValueError):
        normalize_columns(x, eps=-1.0)


def test_rejects_non_matrix(input_array: np.ndarray) -> None:
    with pytest.raises(ValueError):
        normalize_columns(input_array[:, 0])

A pyproject.toml a tanulási profilhoz:

[project]
name = "glyphstats"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["numpy>=2.0"]

[dependency-groups]
dev = ["pytest>=8"]

[tool.pytest.ini_options]
testpaths = ["tests"]

A .gitignore a lokális artefaktumokat zárja ki:

__pycache__/
.pytest_cache/
runs/

A parancsok a projekt gyökeréből:

uv sync
uv run python tools/make_fixture.py
uv run pytest -q
uv run python -m glyphstats.cli \
  --input data/input.npy \
  --output runs/output.npy \
  --manifest runs/manifest.json

A futtatás után a jegyzék szerkezete a következő sémát követi; a hash- és időértékek futásonként generálódnak, ezért itt placeholderként szerepelnek, nem mért outputként:

{
  "run_id": "<generált-uuid>",
  "created_utc": "<ISO-8601 időbélyeg>",
  "input_file": "input.npy",
  "input_sha256": "<64 hex karakter>",
  "output_file": "output.npy",
  "output_sha256": "<64 hex karakter>",
  "params": {"eps": 1e-12, "input_dtype": "float64"},
  "python": "3.12.x",
  "numpy": "2.x.y",
  "machine": "<platform.platform() kimenet>",
  "normalization": "population_std"
}

A README.md első szakasza a 2.3. Izolált környezet, notebook és verzió verziójelentését másolja be: interpreter, kernel, lock-állapot. Így a projekt nemcsak fut, hanem megmondja, milyen feltételek között futott.

2.4.6 Feladatok

F1 (fogalmi, nehézség 1). Magyarázza el, miért nem tiszta függvény egy olyan next_batch(xs, batch_size) függvény, amely modulszintű _cursor globális változót léptet, és adjon olyan hívás-sorrendet, amelyben ugyanaz az argumentum más értéket ad vissza. Elvárt átadandó: definíció szerinti indoklás és konkrét ellenpélda.

F2 (levezetési, nehézség 2). Bizonyítsa be a fenti konvencióval: ha σj>0\sigma_j>0, akkor a standardizált YY oszlopának átlaga 00 és populációs varianciája 11. Kezelje a σj=0\sigma_j=0 esetet is. Elvárt: minden lépés, indexhatár és a degenerált eset indoklása.

F3 (implementációs, nehézség 1). Készítse el a labor projektet, futtassa a négy tesztet, majd a CLI-t. Adja meg a manifest.json run_id, input_sha256 és output_sha256 kulcsait a tényleges futásából. Elvárt átadandó: a parancsok, a tesztkimenet és a manifest három kulcsa.

F4 (hibakeresési, nehézség 2). Az alábbi függvény dokumentációja mintabeli szórást ígér:

def misleading_standardize(x: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Visszaadja a standardizált mátrixot és a mintabeli szórást."""
    mu = x.mean(axis=0)
    sd = x.std(axis=0)          # hibagyanús sor
    denom = np.where(sd > 1e-12, sd, 1.0)
    y = (x - mu) / denom
    return y, sd

Lokalizálja a hibát, mutassa meg a következményét az x=(0,4)x=(0,4) vektoron, javítsa a kódot, és írjon tesztet, amely a javítás előtt megbukna. Elvárt: ok, következmény, javítás, visszaellenőrzés.

F5 (kísérlettervezési, nehézség 3). Tervezzen ellenőrzést arra, hogy a GlyphLab data/images.npy mind a 360 képén a notebookból kiemelt régi függvény és az új modul azonos kimenetet ad-e. Ne futtassa le a teljes kísérletet; írja le a protokollt: input, környezet, a régi és új függvény egy processzben való futtatása, metrika, tolerancia, és a következtetés korlátja. Indokolja, miért nem elegendő az azonos seed, ha a platform megváltozik.

2.4.7 Megoldások és indoklás

F1 megoldás. A tiszta függvény első feltétele sérül: a kimenet függ a modul rejtett állapotától. Legyen xs = [10, 20, 30], _cursor = 0; ekkor next_batch(xs, 1) először [10]-et ad, másodszor [20]-at, holott az argumentumok azonosak. A két hívás között a globális állapot megváltozott, tehát a függvény nem tisztán az argumentumaira épül.

F2 megoldás. Rögzítsük a jj oszlopot, és legyen σj>0\sigma_j>0. A központozott érték Cij=Xij−μjC_{ij}=X_{ij}-\mu_j, a kimenet Yij=Cij/σjY_{ij}=C_{ij}/\sigma_j. Az átlag:

1N∑i=1NYij=1σj(1N∑i=1NXij−μj)=0, \frac1N\sum_{i=1}^N Y_{ij} =\frac{1}{\sigma_j}\left(\frac1N\sum_{i=1}^N X_{ij}-\mu_j\right)=0,

mert a zárójelben μj−μj=0\mu_j-\mu_j=0 áll. A populációs variancia:

1N∑i=1NYij2=1Nσj2∑i=1NCij2=σj2σj2=1, \frac1N\sum_{i=1}^N Y_{ij}^2 =\frac{1}{N\sigma_j^2}\sum_{i=1}^N C_{ij}^2 =\frac{\sigma_j^2}{\sigma_j^2}=1,

mert a központozott értékek populációs varianciája definíció szerint σj2\sigma_j^2. Alakellenőrzés: Y∈ℝN×dY\in\mathbb R^{N\times d}, az átlag oszlopvektor ℝd\mathbb R^d-beli, a variancia nemnegatív skalárok sorozata. Ha σj=0\sigma_j=0, akkor a nemnegatív négyzetösszeg nulla, tehát minden ii-re Cij=0C_{ij}=0, azaz Xij=μjX_{ij}=\mu_j; a definiált konvenció Yij=0Y_{ij}=0, így az átlag 00, a variancia 00, és az 11 érték ebben az esetben nem állítható.

F3 megoldás. A labor a fenti négy tesztet futtatja. A tényleges futásból a manifest.json-t a hallgató olvassa ki; a kulcsok szerkezete a sémában rögzített. A tesztkimenet várhatóan négy sikeres egység, de e kézirat nem állítja, hogy a kódot lefuttatta; a hallgatói munkamenet az elsődleges bizonyíték. Az ellenőrzéshez a run_id 32 hex karakter hosszúságú, mindkét hash 64 hex karakter.

F4 megoldás. A hiba oka: a dokumentáció mintabeli szórást ígér, a NumPy std alapértelmezett ddof=0 értéke viszont populációs normalizálást használ. Következmény az x=(0,4)x=(0,4) vektoron: a mintabeli szórás s=((0−2)2+(4−2)2)/(2−1)=2s=\sqrt{((0-2)^2+(4-2)^2)/(2-1)}=2, a hibás kód viszont 2\sqrt{2}-t ad, tehát a jelentett bizonytalanság kisebb a dokumentált konvenciónál. Javítás: sd = x.std(axis=0, ddof=1). Visszaellenőrző teszt:

def test_misleading_standardize_reports_sample_sd() -> None:
    x = np.array([[0.0], [4.0]])
    _, sd = misleading_standardize(x)
    np.testing.assert_allclose(sd, [2.0], atol=1e-12)

A javítás előtt ez a teszt megbukik, mert 2≠2\sqrt{2}\neq 2; utána a kívánt konvenciót rögzíti.

F5 megoldás (terv, nem mért eredmény). Input: data/images.npy, alak (360,1,24,24), float32; a sorrend a metadata.csv szerint rögzített. A régi függvényt a kísérlet előtt változatlanul kimentjük egy legacy.py fájlba; az új függvényt a projektmodulból importáljuk. Ugyanabban a Python-processzben, ugyanazon a NumPy-verzión és dtype-on futtatjuk mindkettőt minden képen. Metrika: d=maxi|F0(xi)−F1(xi)|d=\max_i |F_0(x_i)-F_1(x_i)|, és az np.allclose(atol=1e-6, rtol=1e-5) eldöntés float32 mellett. A nullhelyzet: a refaktor nem változtatott. A 360 képén mért egyezés csak a tesztelt tartományra empirikus bizonyíték; a teljes tartományra a determinisztikus lemmából következő érvelés ad garanciát. Platformváltáskor az azonos seed nem garantál bitazonosságot [D02], ezért az összehasonlítást ugyanazon gépen kell elvégezni, vagy a toleranciát a platform numerikus zajához kell kalibrálni.

2.4.8 Tipikus tévedés, önellenőrzés és visszahívás

Tipikus tévedés. „A tiszta kód és a zöld teszt garantálja a tudományos állítás helyességét.” Ez hamis. A kódminőség az implementáció kontrollja; a modellértékelés helyessége a konvención, a spliten és a metrikán múlik. Ellenpélda: a F4 hibás függvénye kifogástalanul strukturált, a standardizált outputra épített teszt mégis zöld, mert a hiba a dokumentált statisztikai konvenció és a kód közötti eltérésben van. A teszt csak azt rögzíti, amit explicit módon beleírunk.

Önellenőrzés. (1) Meg tudom-e mondani minden függvényem bemeneti és kimeneti alakját? (2) A teszt a matematikai tulajdonságot ellenőrzi, vagy csak a kód önmagára hivatkozik? (3) A manifest tartalmazza-e a bemeneti hash-t, a környezetet és a paramétereket? (4) A .gitignore kizárja-e a futás közben keletkező fájlokat?

Visszahívás. Az 1.4. Tanulási diagnózis és kísérleti napló megkülönböztette a seedet és a futásazonosítót; itt mindkettő szerephez jut: a fixture a seedből születik, a manifest run_id-ja a napló kulcsa. A 2.3. Izolált környezet, notebook és verzió környezeti jelentése a README első szakasza lett; ezzel a projekt a futtatási feltételeket a kóddal együtt szállítja.

2.4.9 Rövid összefoglaló

A 2.4. Kis tudományos szoftverprojekt a notebookcellát kis szoftverprojektté alakítja: a pathlib a fájlútvonalakat, a modul a tiszta függvényt, a CLI az input–output–manifest szerződést kezeli. A refaktor a bemenet–kimenet leképezést őrzi; a determinisztikus lemma kimondja a feltételeket, a unit teszt ezt véges fixture-ökön ellenőrzi. A zöld teszt nem bizonyítja a teljes tartományt, és nem pótolja a statisztikai konvenció vagy a modellértékelés helyességét. A futási jegyzék hash-t, környezetet és paramétereket rögzít, a lokális artefaktumokat a .gitignore zárja ki. A következő egység a lineáris algebrai alapokat építi fel.

2.4.10 Források