"""Kriterieløperen og løkka. Hører til gribben.no/tech/losninger/loop-og-suksesskriterier.html Et kriterium er en funksjon som svarer bestått eller ikke bestått, og sier hvorfor. Løkka kjører alle kriteriene, skriver en tabell, og kaller en forbedre-funksjon til alt er bestått eller en stoppregel slår inn. Hvorfor kriteriene er funksjoner og ikke en tekst modellen leser: en setning som «koden skal være ferdig» kan alltid regnes som oppfylt. En funksjon kan si nei. En runde er én sjekk av alle kriteriene. Mellom to runder gjør forbedreren ett forsøk. Maks tre runder gir altså tre sjekker og to forsøk. Bare standardbiblioteket, Python 3.10 eller nyere. Prøv det med demo.py. """ from dataclasses import dataclass, field from typing import Callable @dataclass class Kriterium: navn: str sjekk: Callable[[], tuple[bool, str]] # gir (bestått, forklaring) @dataclass class Resultat: navn: str bestatt: bool forklaring: str @dataclass class Stopp: grunn: str # "alle bestått", "ingen framgang", "maks runder", "budsjett" eller "forbedre feilet: …" runder: int brukt: int historikk: list = field(default_factory=list) # én liste med Resultat per runde def kjor_kriterier(kriterier): resultater = [] for k in kriterier: try: bestatt, forklaring = k.sjekk() except Exception as e: # Et kriterium som krasjer, er ikke bestått. Ellers kan en feil i # selve sjekken se ut som at alt er i orden. bestatt, forklaring = False, f"sjekken krasjet: {e}" resultater.append(Resultat(k.navn, bool(bestatt), forklaring)) return resultater def skriv_tabell(runde, resultater, brukt, budsjett, logg=print): bredde = max(len(r.navn) for r in resultater) logg(f"\nRunde {runde} brukt {brukt} av {budsjett}") for r in resultater: merke = "ja " if r.bestatt else "NEI" forste, *resten = r.forklaring.splitlines() or [""] logg(f" {r.navn:<{bredde}} {merke} {forste}") for linje in resten: # lange forklaringer får egne linjer under logg(f" {'':<{bredde}} {linje}") def lokke(kriterier, forbedre, maks_runder=5, budsjett=10_000, logg=print): """Kjør kriteriene, og kall forbedre(resultater) til alt er bestått. forbedre får resultatene fra runden som tilbakemelding og gir tilbake hva forsøket kostet. Løkka stopper på én av fem måter, og sier hvilken. """ if not kriterier: raise ValueError("Ingen kriterier. Skriv hva ferdig betyr før løkka starter.") brukt = 0 historikk = [] sett = [] # feilbildet fra hver tidligere runde for runde in range(1, maks_runder + 1): resultater = kjor_kriterier(kriterier) historikk.append(resultater) skriv_tabell(runde, resultater, brukt, budsjett, logg) # Hvilke kriterier feiler, og hvorfor. Forklaringen må si hva som # feiler, ikke bare hvor mye, ellers ser to ulike feil like ut. feil = [(r.navn, r.forklaring) for r in resultater if not r.bestatt] if not feil: return Stopp("alle bestått", runde, brukt, historikk) if feil in sett: # Samme feilbilde som i en tidligere runde, enten rett før eller # lenger tilbake (løkka svinger). Da skal et menneske se på det. return Stopp("ingen framgang", runde, brukt, historikk) if runde == maks_runder: return Stopp("maks runder", runde, brukt, historikk) if brukt >= budsjett: # Sjekkes før neste forsøk. Det siste forsøket kan likevel ha gått # over grensa, for kostnaden er først kjent når det er gjort. return Stopp("budsjett", runde, brukt, historikk) sett.append(feil) try: brukt += int(forbedre(resultater)) except Exception as e: # Med en ekte modell er dette et API-kall, og det kan feile. # Da stopper løkka med det den har, i stedet for å krasje. logg(f" -> forbedre feilet: {e}") return Stopp(f"forbedre feilet: {e}", runde, brukt, historikk) return Stopp("maks runder", maks_runder, brukt, historikk) # nås bare med maks_runder < 1