Python für Anfänger · Teil 21TutorialEinsteiger

Python

Python für Anfänger – Teil 21: Abschlussprojekt: deine eigene Filamentverwaltung

Verbinde die Kursgrundlagen zu einer vollständigen Konsolenanwendung mit Eingabeprüfung und JSON-Speicherung.

Veröffentlicht am

5 Minuten Lesezeit

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