Voraussetzungen
Du kennst die Inhalte aus Teil 20 und arbeitest mit einem lokalen Python-3-Interpreter (ab Python 3.10), beispielsweise in Thonny.
Das lernst du in dieser Lektion
- Funktionen zu einer Anwendung verbinden.
- Bestandsänderungen prüfen und speichern.
- Das Programm anhand fester Fälle testen.
Aus einzelnen Grundlagen wird ein Programm
Jetzt ergänzen wir die Zwischenversion um alle geplanten Funktionen. Arbeite in einem neuen Ordner teil-21. Speichere den vollständigen Code unten als filamentverwaltung.py. Die Datei bestand.json entsteht nach dem ersten Speichern neben dem Skript. Zusätzliche Pakete sind nicht nötig.
Beim allerersten Start ist der Bestand leer. Wähle 2 und lege eine Rolle an, zum Beispiel PLA, Blau und 750 g. Mit 1 zeigst du sie an, mit 3 buchst du einen Verbrauch. 4 speichert ohne Beenden; 0 speichert und beendet nur dann, wenn das Speichern erfolgreich war.
Die neuen Bausteine verstehen
frage_text() wiederholt eine Eingabe, bis nach dem Entfernen äußerer Leerzeichen Text vorhanden ist. frage_menge() unterstützt Punkt oder Komma als Dezimalzeichen, fordert aber eine Zahl ohne Tausendertrennzeichen. Es weist Sonderwerte wie nan oder inf zurück. Bei einer Restmenge darf 0 erlaubt sein, bei einem Verbrauch nicht.
pruefe_bestand() kontrolliert auch geladene Daten. isinstance() prüft den Typ. set(rolle) bildet die Menge der vorhandenen Dictionary-Schlüssel; der Vergleich verlangt genau die drei vorgesehenen Felder. Ein Set ist eine ungeordnete Sammlung eindeutiger Elemente. Wir verwenden es hier nur für diese Feldprüfung.
Bei einer Mengenprüfung müssen wir Wahrheitswerte ausdrücklich zurückweisen: bool ist in Python ein Untertyp von int. Eine Rolle mit rest_g: true ist trotzdem kein sinnvoller Materialbestand. Der zusätzliche OverflowError-Zweig behandelt extrem große Ganzzahlen aus manuell bearbeiteten Dateien; für normale Grammwerte spielt er keine Rolle. from None unterdrückt dabei die Verkettung mit der ursprünglichen Ausnahme, damit die fachliche Meldung übersichtlich bleibt.
Vollständiger Programmcode
"""Lernprojekt: lokaler Filamentbestand, Python 3.10+, nur Standardbibliothek."""
import json
import math
from pathlib import Path
def frage_text(frage):
while True:
text = input(frage).strip()
if text:
return text
print("Die Eingabe darf nicht leer sein.")
def frage_menge(frage, null_erlaubt=False):
while True:
text = input(frage).strip().replace(",", ".")
try:
menge = float(text)
except ValueError:
print("Bitte eine Zahl ohne Tausendertrennzeichen eingeben.")
continue
if not math.isfinite(menge):
print("Bitte eine endliche Zahl eingeben.")
continue
if menge < 0 or (menge == 0 and not null_erlaubt):
print("Die Menge muss positiv sein." if not null_erlaubt else "Die Menge darf nicht negativ sein.")
continue
return menge
def pruefe_bestand(bestand):
if not isinstance(bestand, list):
raise ValueError("Der Bestand muss eine Liste sein.")
for nummer, rolle in enumerate(bestand, start=1):
if not isinstance(rolle, dict):
raise ValueError(f"Eintrag {nummer} muss ein Dictionary sein.")
# In dieser Kursversion sind genau diese drei Felder vorgesehen.
if set(rolle) != {"material", "farbe", "rest_g"}:
raise ValueError(f"Eintrag {nummer} hat fehlende oder unbekannte Felder.")
for feld in ("material", "farbe"):
if not isinstance(rolle[feld], str) or not rolle[feld].strip():
raise ValueError(f"Eintrag {nummer}: {feld} muss Text enthalten.")
rest = rolle["rest_g"]
# bool ist in Python ein Untertyp von int, hier aber keine Materialmenge.
if isinstance(rest, bool) or not isinstance(rest, (int, float)):
raise ValueError(f"Eintrag {nummer}: rest_g muss eine Zahl sein.")
try:
endlich = math.isfinite(rest)
except OverflowError:
raise ValueError(f"Eintrag {nummer}: Zahlenwert ist zu groß.") from None
if not endlich or rest < 0:
raise ValueError(f"Eintrag {nummer}: rest_g muss endlich und nicht negativ sein.")
def laden(pfad):
try:
with pfad.open("r", encoding="utf-8") as datei:
bestand = json.load(datei)
except FileNotFoundError:
return []
pruefe_bestand(bestand)
return bestand
def speichern(pfad, bestand):
zwischenpfad = pfad.with_suffix(".tmp")
try:
pruefe_bestand(bestand)
with zwischenpfad.open("w", encoding="utf-8") as datei:
json.dump(bestand, datei, ensure_ascii=False, indent=2, allow_nan=False)
# Erst die vollständig geschriebene Datei ersetzt den bisherigen Stand.
zwischenpfad.replace(pfad)
except (OSError, ValueError) as fehler:
print(f"Speichern fehlgeschlagen: {fehler}")
return False
print(f"Gespeichert: {pfad}")
return True
def anzeigen(bestand):
if not bestand:
print("Noch keine Rollen vorhanden.")
return
for nummer, rolle in enumerate(bestand, start=1):
print(f"{nummer}: {rolle['material']} / {rolle['farbe']} - {rolle['rest_g']:.1f} g")
def hinzufuegen(bestand):
material = frage_text("Material: ").upper()
farbe = frage_text("Farbe: ")
rest_g = frage_menge("Restmenge in g (ab 0): ", null_erlaubt=True)
bestand.append({"material": material, "farbe": farbe, "rest_g": rest_g})
print("Rolle hinzugefügt. Zum dauerhaften Übernehmen speichern.")
def verbrauchen(bestand):
if not bestand:
print("Noch keine Rollen vorhanden.")
return
anzeigen(bestand)
try:
nummer = int(input("Rollennummer: "))
except ValueError:
print("Bitte eine ganze Rollennummer eingeben.")
return
if not 1 <= nummer <= len(bestand):
print("Diese Rolle gibt es nicht.")
return
rolle = bestand[nummer - 1]
verbrauch_g = frage_menge("Verbrauch in g (größer als 0): ")
if verbrauch_g > rolle["rest_g"]:
print("Verbrauch ist höher als der Bestand. Keine Änderung.")
return
rolle["rest_g"] -= verbrauch_g
print(f"Neuer Rest: {rolle['rest_g']:.1f} g. Bitte speichern.")
def main():
pfad = Path(__file__).resolve().parent / "bestand.json"
try:
bestand = laden(pfad)
except (OSError, ValueError, UnicodeError) as fehler:
print(f"Bestand konnte nicht geladen werden: {fehler}")
print("Die Originaldatei bleibt unverändert. Bitte prüfen und erneut starten.")
return
while True:
print("\n1: Anzeigen | 2: Rolle hinzufügen | 3: Verbrauch buchen")
print("4: Speichern | 0: Speichern und beenden")
auswahl = input("Auswahl: ").strip()
if auswahl == "1":
anzeigen(bestand)
elif auswahl == "2":
hinzufuegen(bestand)
elif auswahl == "3":
verbrauchen(bestand)
elif auswahl == "4":
speichern(pfad, bestand)
elif auswahl == "0":
if speichern(pfad, bestand):
break
print("Das Programm bleibt geöffnet. Der Bestand ist noch im Arbeitsspeicher.")
else:
print("Unbekannte Auswahl.")
if __name__ == "__main__":
main()
Laden und Speichern nachvollziehen
laden() startet nur bei fehlender Datei mit einer leeren Liste. Bei beschädigtem JSON oder unpassender Struktur beendet sich das Programm mit einer Erklärung. Es ersetzt die problematische Datei nicht durch einen leeren Bestand. So kannst du sie zuerst kontrollieren.
speichern() schreibt zunächst bestand.tmp im selben Ordner. Erst nach erfolgreichem Schreiben ersetzt diese Datei den bisherigen JSON-Stand. .with_suffix() ändert die Dateiendung eines Pfadobjekts; .replace() ersetzt die Zieldatei. Schlägt der Vorgang fehl, meldet die Funktion False. Bei Auswahl 0 bleibt das Menü dann geöffnet.
Dieses Vorgehen reduziert das Risiko einer beim Schreiben abgeschnittenen Bestandsdatei. Es ersetzt kein Backup und ist keine Absicherung gegen alle Ausfälle. Starte nur eine Instanz des Übungsprogramms pro Datenordner. Unterbrich es nicht absichtlich während des Speicherns.
Verbrauch korrekt zuordnen
Die Oberfläche zeigt Nummern ab 1, eine Liste beginnt bei Index 0. Deshalb greift bestand[nummer - 1] auf die ausgewählte Rolle zu. Vorher wird der zulässige Bereich geprüft. Ein Verbrauch über dem Restbestand wird vollständig abgelehnt; es wird nichts teilweise abgezogen.
Die Rolle ist ein Dictionary in der Bestandsliste. Die Zuweisung an rolle["rest_g"] verändert genau diesen Eintrag. Dauerhaft wird die Änderung erst nach Auswahl 4 oder einem erfolgreichen Beenden mit 0.
Prüflauf zum Mitmachen
| Schritt | Aktion | Erwartetes Ergebnis |
|---|---|---|
| 1 | Ohne Bestandsdatei starten, 1 wählen | Leerhinweis |
| 2 | Rolle PLA/Blau/750 anlegen | Eine Rolle im Bestand |
| 3 | 74 g von Rolle 1 abbuchen | 676.0 g Rest |
| 4 | 800 g von derselben Rolle abbuchen | Ablehnung, weiterhin 676.0 g |
| 5 | Rolle 0 oder 9 wählen | Hinweis, unveränderter Bestand |
| 6 | Bei Menge abc, -1, nan oder inf eingeben | Ablehnung und erneute Abfrage |
| 7 | 4 wählen, danach 0 und neu starten | 676.0 g werden wieder geladen |
| 8 | In einer Testkopie bestand.json beschädigen | Ladehinweis, Datei bleibt unverändert |
Teste beschädigte Dateien nur in einer Kopie des gesamten Projektordners. Für einen Mengenvergleich mit mehreren Nachkommastellen gelten weiterhin die Eigenschaften von Gleitkommazahlen. Die Oberfläche zeigt eine Nachkommastelle; intern wird nicht nach jedem Verbrauch gerundet.
Wie könnte es weitergehen?
Mögliche eigene Erweiterungen sind eine Suchfunktion, ein CSV-Export, feste Rollen-IDs oder eine Verbrauchshistorie mit Datum. Setze jeweils nur eine Änderung um und wiederhole danach die vorhandenen Prüffälle. Eine grafische Oberfläche kann später dieselben fachlichen Bausteine verwenden.
Mit diesem Abschlussprojekt hast du nicht nur einzelne Befehle ausprobiert: Du hast Eingaben verarbeitet, Entscheidungen formuliert, Daten strukturiert und einen nachvollziehbaren Ablauf mit dauerhafter Ablage gebaut.
Mitmach-Aufgabe
Führe die Prüftabelle durch. Ergänze anschließend im Anzeigemenü eine Gesamtmenge aller Rollen. Lege zum Prüfen zwei Rollen mit 500 und 300 g an.
Versuche die Aufgabe zuerst selbst. Vergleiche danach deinen Ansatz mit der Musterlösung. Andere Lösungen sind möglich, wenn sie dieselben Anforderungen erfüllen.
Musterlösung
# Am Ende von anzeigen(), nach der for-Schleife, ergänzen:
gesamt_g = 0
for rolle in bestand:
gesamt_g += rolle["rest_g"]
print(f"Gesamtmenge: {gesamt_g:.1f} g")
Ergebnis prüfen
Für die beiden Übungsrollen erscheint nach ihren Einzelzeilen Gesamtmenge: 800.0 g. Die Ergänzung steht innerhalb von anzeigen(), aber außerhalb der vorhandenen for-Schleife.
Typische Stolperstellen
Änderungen im Arbeitsspeicher sind noch keine gespeicherte Datei. Die Anwendung ist für einen lokalen Lernbestand ausgelegt. Bei einem Schreibfehler nicht einfach schließen, sondern Ursache prüfen und erneut speichern.
Was du jetzt kannst
- Funktionen zu einer Anwendung verbinden.
- Bestandsänderungen prüfen und speichern.
- Das Programm anhand fester Fälle testen.
Passendes Cheatsheet
Zum Nachschlagen: Cheatsheet 14 – CSV und JSON mit Python. Dort findest du auch den vorhandenen PDF-Download.
Im nächsten Teil
Der Grundlagenkurs ist abgeschlossen. Wähle eine der genannten Erweiterungen und entwickle sie in kleinen, prüfbaren Schritten.
← Teil 20 · Kursübersicht · Kurs abgeschlossen
