Tag 4 - Debugger, Exceptions & I/O

Tag 4 - Debugger, Exceptions & I/O#

Schematische Darstellung einer while-Schleife in Python

Bis heute haben wir die Grundlagen zu Datentypen kennengelernt, wissen wie wir sie Variablen zuweisen können und wie wir Code wiederverwendbar machen, indem wir Funktionen definieren. Während der letzten Tage haben wir zudem erste Fehlermeldungen kennen und lieben gelernt. Und weil wir Fehler so sehr lieben gelernt haben, gibt es davon heute noch mehr: wie diese ggf. schon automatisch verarbeitet werden können, als auch etwas weniger kryptischer erscheinen. Zudem schauen wir uns ein mächtiges Werkzeug zum besseren Nachvollziehen von Fehlerquellen an - den DEBUGGER.

Debugger#

Der Debugger, wie klar ersichtlich, kann im ersten Moment etwas einschüchternd wirken, möchte jedoch im Kern nur helfen.

Ein Debugger ist ein durchaus mächtiges Werkzeug, das Programmierern hilft, Fehler (auch bekannt als Bugs) in ihrem Code zu finden und zu beheben.

Ein Debugger führt ein Python-Dokument etwas aus als es normalerweise der Fall wäre. Der Debugger ermöglicht es Ihnen Ihren Code Zeile für Zeile, Schritt für Schritt, auszuführen und durchzugehen und den aktuellen Zustand des Programms zu überprüfen. Dies hilft dabei, den Ablauf des Programms zu verstehen und potenzielle Fehlerquellen zu identifizieren.

Zusätzlich können die einzelnen Variablen zu verschiedenen Zeitpunkten während der Ausführung inspiziert werden. Dies ist besonders nützlich, um festzustellen, ob Variablen die erwarteten Werte haben und ob sich diese Werte im Laufe des Programms ändern.

Um genau an den Punkt springen zu können, der überprüft werden soll, können Haltepunkte, s.g. breakpoints gesetzt werden. Wenn etwa. ein breakpoint in Zeile 10 gesetzt wird, läuft das Programm bis hier und stoppt dann in der Laufzeit. Besonders ist, dass dennoch alle Werte der Variablen zugänglich sind und von hier an der Code manuell Schritt für Schritt durchgegangen werden kann.

Ein Debugger kann auch automatisch an Orten im Code stoppen, in denen es zu Fehlern kommt. Hier liefert der Debugger eine detaillierte Rückverfolgung des sogenannten “Stack-Traces”, der anzeigt, welche Funktionen zu welchem Zeitpunkt aufgerufen wurden. Dies kann ermöglichen den Kontext des Fehlers besser zu verstehen und so schneller zu beheben.

Insgesamt ermöglicht ein Debugger es Ihnen, Ihren Code effizienter zu debuggen, Fehler schneller zu finden und zu beheben und ein tieferes Verständnis für die Funktionsweise ihres Programms zu entwickeln. Es ist ein unverzichtbares Werkzeug für jede:n Programmierer:In, unabhängig von ihrem Erfahrungslevel.

Beispiele:

a = 4
b = 10

add = addition(a, b)
c = a
c = 5
b += b
d = c
a = d
add = addition(a, b)

multi = multiplikation(a, b)
l = create_list(False)

def addition(x,y):
    return x + y

def multiplikation
    x *= y
    return x

def create_list():
    if lc:
        my_list = [element for element in range(10, 31)]
    else:
        my_list = []
        for element in range(10,31):
            my_list.append(element)

Es war Ostern! Was wurde denn alles gefunden?

  1. Zuerst wollen wir rausfinden wieviele Ostereier tatsächlich gefunden wurden. Aber irgendwie stimmt da doch was nicht..

ostereier_pro_person = [5, 8, 15, 3]

total_eggs = 0
for person in range(1,4):
    eggs = [ostereier_pro_person[person]]
    total_eggs += person  
print("Du hast insgesamt", total_eggs, "Ostereier gefunden.")
  1. Wer hat denn die meisten Ostereier pro Minute gefunden (Durchschnittswert)? Struktur des Dictionary: {‘Name’: (ostereier, minuten_gesucht)}

 personen: {'Anna': (5, 5), 'Benny': (8, 7), 'Carla': (15, 12) 'David': (3, 4)}
 
 max_pro_minute = 0
 beste_person = ""
 
 for name, (ostereier, minute) in personen.items():
     ostereier_pro_minute = ostereier // minute
     if ostereier_pro_minute >= max_pro_minute:
         max_pro_minute += ostereier_pro_minute
         beste_person = name
 
 print("Die Person mit den meisten Ostereiern pro Minute ist", beste_person)

# Wer müsste es denn überhaupt sein?
# (optional) Wie speichere ich die Personen mit dem errechneten Durchscnittswert zusammen ab?

Und wer hat die leckersten Schokoeier gefunden?

import random as rnd

def bewertung():
   '''Bewertet Schokoeier eins nach dem anderen'''
    schokoei = schokoei()
    
    if schokoei == 0:
        print("gar nicht mal so lecker")
    else schokoei is 1:
        print("Nicht gut, aber ansatzweise essbar")
    else schokoei > 2:
        print("essbar aber noch nicht wirklich lecker")
    else schokoei == 4:
        print("Richtig gut!")
    else schokoei != 5:
        print("Absoluter Himmel")
    else:
        print("nicht auf der Skala enthalten")

def schokoei():
    '''Gibt einen random Wert der Möglichkeiten 1,2,3,4,5 aus'''
    return rnd.randint(1,5)

bewertung()

Klassen#

Klassen gehören zum übergeordneten Themenbereich der Objekt-Orientierten-Programmierung (OOP) welches wir in diesem Kurs nur wenig streifen werden. Um den Aufbau von Exceptions jedoch besser verstehen zu können hier eine kleine Einführung zu Klassen.

Klassen in Python sind Bausteine, die es dir ermöglichen, Daten und Funktionalität zu organisieren und zu kapseln. Eine Klasse definiert das Verhalten eines bestimmten Objektes, indem sie Attribute (Daten) und Methoden (Funktionen) kombiniert, die auf dieses Objekt angewendet werden können. Hier ist ein einfaches Beispiel, das eine Klasse Person erstellt:

class Person:
    def __init__(self, name, alter):
        self.name = name
        self.alter = alter

    def geburtstag_feiern(self):
        self.alter += 1

In diesem Beispiel:

  • Das class-Schlüsselwort definiert die Klasse Person.

  • Die Methode init() ist ein Konstruktor, der aufgerufen wird, wenn eine Instanz der Klasse erstellt wird. Hier werden die Attribute name und alter für jedes Objekt initialisiert.

    • Eine Instanz einer Klasse ist das konkrete Objekt einer Klasse. Sie können sich das so vorstellen, das eine Klasse im wesentlichen ein Bauplan oder Schablone ist, der in diesem Fall beschreibt das ganz generell zu einer Person ein Name und ein Alter gehören. Eine Instanz ist nun ein konkretes Objekt, welches auf dieser Schablone basiert - also die eigentliche Person. Von einer Klasse können so mehrere Personen erstellt werden, die jeweils unterschiedliche Namen und Alter haben können, jedoch alle diese Variablen haben. Wenn somit bekannt ist. Ein anderes Beispiel wäre der Bauplan eines Autos (Klasse) und das eigentliche Auto selbst (Instanz). Ein Auto hat etwa. immer vier Räder (festgelegt in der Klasse), jedoch kann die Größe immer unterschiedlich sein (je Fahrzeug(typ)).

  • self ist eine Referenz auf das aktuelle Objekt. Es wird automatisch an alle Methoden als erstes Argument übergeben.

  • self.name und self.alter sind Attribute, die an jedes Objekt der Klasse gebunden sind.

  • geburtstag_feiern() ist eine Methode, die das Alter einer Person erhöht.

Nachdem wir die Klasse definiert haben, können wir Instanzen davon erstellen und auf ihre Attribute und Methoden zugreifen.

Vererbung#

Vererbung ist ein zentrales Konzept in der objektorientierten Programmierung, das es ermöglicht, Eigenschaften und Verhaltensweisen einer vorhandenen Klasse zu übernehmen und in einer neuen Klasse zu erweitern oder anzupassen. Dabei wird eine Beziehung zwischen den Klassen definiert, wobei die Klasse, die erbt, als die abgeleitete Klasse oder Subklasse bezeichnet wird, und die Klasse, von der sie erbt, als die Basisklasse oder Superklasse.

Um Vererbung anhand der Klassen Person, Student und Dozent zu erklären, nehmen wir an, dass Person die Basisklasse ist, und Student und Dozent erben von Person. Hier ist ein Beispiel, wie das aussehen könnte:

class Person:
    def __init__(self, name, alter):
        self.name = name
        self.alter = alter

    def info_anzeigen(self):
        print(f"Name: {self.name}, Alter: {self.alter}")

class Student(Person):
    def __init__(self, name, alter, matrikelnummer):
        super().__init__(name, alter)
        self.matrikelnummer = matrikelnummer

    def studienbescheinigung_erstellen(self):
        print(f"{self.name} hat die Matrikelnummer {self.matrikelnummer}")

class Dozent(Person):
    def __init__(self, name, alter, fachgebiet):
        super().__init__(name, alter)
        self.fachgebiet = fachgebiet

    def vorlesung_halten(self):
        print(f"{self.name} hält eine Vorlesung zum Thema {self.fachgebiet}")

In diesem Beispiel dient die Klasse Person als Basisklasse und enthält grundlegende Informationen wie Name und Alter sowie eine Methode info_anzeigen(), um diese Informationen auszugeben. Die Klassen Student und Dozent erben von Person, was bedeutet, dass sie alle Attribute und Methoden von Person automatisch übernehmen. Sie können jedoch auch zusätzliche Attribute und Methoden haben, die spezifisch für Studierende bzw. Dozenten sind. Die init()-Methoden der abgeleiteten Klassen rufen den Konstruktor der Basisklasse (super().init()) auf, um sicherzustellen, dass die Attribute der Basisklasse initialisiert werden. Die abgeleiteten Klassen können zusätzliche Methoden haben, die spezifisch für ihre Rolle sind, wie z.B. studienbescheinigung_erstellen() für Student und vorlesung_halten() für Dozent.

Beispiel der Verwendung:

studentin1 = Student("Alice", 20, "12345")
dozent1 = Dozent("Jakob", 999, "Geoinformatik")

student1.info_anzeigen()  # Ausgabe: Name: Alice, Alter: 20
student1.studienbescheinigung_erstellen()  # Ausgabe: Alice hat die Matrikelnummer 12345

dozent1.info_anzeigen()  # Ausgabe: Name: Jakob, Alter: 999
dozent1.vorlesung_halten()  # Ausgabe: Jakob hält eine Vorlesung zum Thema Geoinformatik

Auf diese Weise können wir den Code besser strukturieren und wiederverwendbaren Code fördern, indem wir die gemeinsame Funktionalität in der Basisklasse Person definieren und sie dann in den abgeleiteten Klassen Student und Dozent erweitern. Klassen ermöglichen so Funktionen und Daten zu bündeln und weiterverwenden zu können.

Exceptions#

Ausnahmen (Exceptions) sind Ereignisse, die während (!!) der Programmausführung auftreten und den normalen Ablauf des Programms unterbrechen. Diese Ereignisse können Fehlerbedingungen sein, wie z.B. das Teilen durch Null oder das Zugreifen auf einen ungültigen Index. In Python können Ausnahmen mit try, except und finally behandelt werden, damit es nicht immer automatisch zu einem Programmabsturz kommt, sondern erst andere Lösungen versucht werden können. Folgende Beispiele lehnen sich an dieses Repo an.

print(1/0) # Syntaktisch ist es richtig, aber glauben Sie, dass es ausgeführt werden kann?

print(name) # haben wir vor dieser Codezeile deklariert/definiert?

print('2' + 5) # Was wird das Ergebnis sein?

Um die oben genannten Beispiele zu überwinden, erstelle ich eine Methode, die die Ausnahmen darin behandelt.

def division():
    try:
        print(1/0)
    except ZeroDivisionError:
        print("Warum versuchen Sie, eine natürliche Zahl durch 0 zu dividieren?")

division()

Hier wird versucht durch ‘0’ zu teilen, was einen ZeroDivisionError wirft (Errors werden immer ‘geworfen’). Der Except-Block fängt diesen achtlos hingeworfenen Error auf und führt das Statement print("Warum versuchen Sie, eine natürliche Zahl durch 0 zu dividieren?") aus. Hier könnte natürlich auch etwas sinnvolleres stehen, was tatsächlich das Problem löst - das wäre aber natürlich viel zu einfach. Der anschließende finally-Block wird immer ausgeführt, unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht.

Wichtig ist allerdings, dass in diesem Fall nur ZeroDivisionError Exceptions vom except-Block aufgefangen werden. Wenn wir bspw. einen anderen Fehler produzieren wird dieser nicht gefangen:

Zweites Beispiel: Wichtig zu wissen - wie auch in normalem Code, wie bspw. bei if-else-Blöcken, werden die Exceptions von oben nach unten getestet.

def showName():
    try:
        print(name)
    except ZeroDivisionError:
        print("why are you trying divide a natural number with 0?")
    except ValueError:
        print("Value error man..")
    except TimeoutError:
        print("time out error sir..")
    except NameError:
        print("You haven't defined any such variable btw")
    # Stlyish way is..
    except Exception:
        print("I catch (mostly) everything.")
        pass

showName()

Exceptions können recht breit oder recht eng definiert werden und nur entsprechend Fehler auffangen. Der ZeroDivisionError wird somit ausschließlich entsprechende Fehler auffangen, die durch die Division durch Null entstehen. Welcher Except-Block wird somit im zweiten, sowie kommenden dritten und vierten Beispiel ausgeführt? Warum?

Drittes Beispiel:

def addValues():
    try:
        print('2' + 'Sanjay' + 1232)
    except TypeError:
        print("Ich unterstütze diese Fehlerart nicht.")
    except Exception:
        print('Der Meister sagt: "Irgendetwas ist im Code schief gelaufen."')

addValues()

Viertes Beispiel: Was wird jetzt ausgegeben?

def addValues2():
    try:
        print('2' + 'Sanjay' + 1232)
    except Exception:
        print('Irgendetwas ist im Code schief gelaufen.')
    except TypeError:
        print("Ich unterstütze diese Fehlerart nicht.")
    else:
        print("kein Fehler geworfen worden")
    finally:
        print("Dieses Statement wird IMMER ausgeführt. Vollkommen egal, was voher passiert ist.")

addValues2()

Exceptions können auch selbst definiert werden - darauf gehen wir jedoch nicht ein im Kontext dieses Kurses. Die hier genutzten vordefinierten Exceptions sind durch das Prinzip der Vererbung miteinander verknüpft. Somit gibt es auch hier von Exceptions Basis- und Subklassen. Die Basisklasse aller Exceptions ist die s.g. Exception selbst. Hiervon erben alle anderen Exceptions. Wichtig dabei zu wissen ist, dass eine Klasse, von der eine Klasse erbt, auch deren Fehler fängt - nur nicht so detailliert. Dies ist bspw. im Vierten Beispiel zu sehen. Hier wird der Fehler im except-Block der Exception gefangen und verarbeitet - soweit scheinbar erstmal nicht schlimm, jedoch fehlt uns in der Verarbeitung die Information, was für ein Fehler uns vor die Füße geworfen wurde. Das Auffangen von spezifischen Exceptions wie dem ZeroDivisionError gleicht dabei einer sehr detaillierten Hilfestellung bei der uns gesagt wird wo und was genau schief gelaufen ist. Das Fangen einer allgemeinen Exception hingegen ist eher vergleichbar mit einem Kommilitonen, der Ihnen sagen würde “Dein Code funktioniert nicht” - wenig hilfreich. Auch würde eine solche Exception ggf. Fehler auffangen, die uns gar nicht bewusst sind. Somit würde der Code nicht mal einen Fehler werfen, sondern nur einfach nicht funktionieren - noch schlimmer.

Daher ist es wichtig zu beachten, welche Exceptions gefangen wie gefangen werden wollen, und welche nicht. Wenn es in einem Programm bspw. zu einem ZeroDivisionError kommt, ist mit Sicherheit schon an einer anderen Stelle etwas schief gelaufen und das Programm sollte beendet, repariert und neu gestartet werden. Denken Sie daran - Fehlermeldungen sind Hilfestellungen und keine Rüge!

Note: Exceptions können durch das Schlüsselwort raise auch selbst geworfen werden. Das Schlüsselwort assert ermöglicht es wie mit einer if Abfrage Statements zu evaluieren und entsprechend einen AssertionError zu werfen - bitte nur im Debbuging verwenden!

Die Vererbungsabhängigkeiten können somit als Baumstruktur dargestellt werden:

BaseException
 ├── BaseExceptionGroup
 ├── GeneratorExit
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
          ├── FloatingPointError
          ├── OverflowError
          └── ZeroDivisionError
      ├── AssertionError
      ├── AttributeError
      ├── BufferError
      ├── EOFError
      ├── ExceptionGroup [BaseExceptionGroup]
      ├── ImportError
          └── ModuleNotFoundError
      ├── LookupError
          ├── IndexError
          └── KeyError
      ├── MemoryError
      ├── NameError
          └── UnboundLocalError
      ├── OSError
          ├── BlockingIOError
          ├── ChildProcessError
          ├── ConnectionError
              ├── BrokenPipeError
              ├── ConnectionAbortedError
              ├── ConnectionRefusedError
              └── ConnectionResetError
          ├── FileExistsError
          ├── FileNotFoundError
          ├── InterruptedError
          ├── IsADirectoryError
          ├── NotADirectoryError
          ├── PermissionError
          ├── ProcessLookupError
          └── TimeoutError
      ├── ReferenceError
      ├── RuntimeError
          ├── NotImplementedError
          └── RecursionError
      ├── StopAsyncIteration
      ├── StopIteration
      ├── SyntaxError
          └── IndentationError
               └── TabError
      ├── SystemError
      ├── TypeError
      ├── ValueError
          └── UnicodeError
               ├── UnicodeDecodeError
               ├── UnicodeEncodeError
               └── UnicodeTranslateError
      └── Warning
           ├── BytesWarning
           ├── DeprecationWarning
           ├── EncodingWarning
           ├── FutureWarning
           ├── ImportWarning
           ├── PendingDeprecationWarning
           ├── ResourceWarning
           ├── RuntimeWarning
           ├── SyntaxWarning
           ├── UnicodeWarning
           └── UserWarning
# source: https://docs.python.org/3/library/exceptions.html#exception-hierarchy

Note: Es gibt Fehler wie den SyntaxError, die vor der Ausführung des eigentlichen Programmes erkannt und geworfen werden. Diese können durch diesen Try-Except-Block nicht aufgefangen werden und werden immer geworfen - was auch wünschenswert ist, da mit solchen Fehler schwer im Programm umzugehen ist. Überlegen Sie sich also immer gut, ob und wie sie einen Fehler auffangen wollen.

Ein IndexError könnte somit auch von einem LookupError oder der Exception selbst gefangen werden. Was wäre am sinnvollsten?

Angenommen Sie würden einen FileNotFoundError bekommen. Mit welchen anderen Exceptions könnten Sie diesen Fehler noch auffangen?

Fehlersuche

  1. Sie haben folgenden Code:

list1 = [1, 2, 3]
print(list1[10])

Welcher Fehler wird geworfen? Fangen Sie diesen Fehler nun sinnvoll und detailliert auf und geben Sie eine sinnvolle Fehlermeldung aus.

  1. Sie bekommen einen AttributeError während Sie das Studienfach der Studentin Alice von oben abfragen wollten. Warum? Gehen Sie entsprechend mit diesem Fehler um.

studentin1 = Student("Alice", 20, "12345")
'''
Ausgabe:
studentin1.studienfach_anzeigen()
AttributeError: 'Student' object has no attribute 'studienfach_anzeigen'
'''
  1. (advanced only) Sie haben folgenden Code:

class Shape:
    def area(self):
        raise NotImplementedError("Die Methode area() muss in einer Unterklasse implementiert werden.")

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

Was wird hier gemacht? Das Areal des Rechtecks und des Kreises ist gefragt.

rectangle = Rectangle(5, 4)
print(rectangle.area())

circle = Circle(3)
circle.area()

Funktioniert das? Wenn ein Fehler geworfen wird, welcher ist es und wie gehen Sie damit am besten um?

I/O#

I/O steht für Input/Output und bezieht sich auf den Austausch von Daten zwischen einem Computerprogramm und der Außenwelt. Dabei kann es sich um das Lesen von Daten aus einer Datei, die Eingabe durch den Benutzer über die Tastatur oder Maus, das Senden von Daten über das Netzwerk oder das Anzeigen von Informationen auf dem Bildschirm handeln. Jetzt behandeln wir nur das eigenständige Eingeben durch einen Benutzer, sowie das Lesen und Schreiben einer Datei.

Um auf eine Datei zugreifen zu können, muss dem Programm gesagt werden, wo diese Datei auf dem PC liegt. Hierbei wird zwischen absoluten und relativen Pfaden unterschieden, um Dateien und Verzeichnisse zu referenzieren:

Absolute Pfade: Ein absoluter Pfad gibt den vollständigen Speicherort einer Datei oder eines Verzeichnisses an, beginnend vom Stammverzeichnis des Dateisystems. Dies bedeutet, dass der Pfad unabhängig von der aktuellen Arbeitsverzeichnisposition immer den gleichen Speicherort identifiziert.

Relative Pfade: Ein relativer Pfad gibt den Speicherort einer Datei oder eines Verzeichnisses relativ zum aktuellen Arbeitsverzeichnis an. Das heißt, er bezieht sich als Ausgangsort auf den Speicherort der ausführenden Datei (bspw. einlesen.py).

Sie können sich das in etwa mit einer ‘normalen’ Wegbeschreibung vorstellen. Nehmen Sie dafür an, dass das Stammverzeichnis von Heidelberg der HBF ist. Wenn Sie jemandem somit den absoluten Pfad zur Neuenheimer Mensa beschreiben würden, dann würden Sie vom HBF ausgehend den Weg beschreiben. Ein Relativer Pfad würde im Gegensatz dazu von Ihrem momentanen Standort (bspw. Berliner Str.) aus beginnen. Beide Arten von Pfaden haben ihre Vor- und Nachteile.

In Python gibt es verschiedene Möglichkeiten, I/O zu implementieren:

  1. Interaktion mit dem Benutzer über die Konsole:

name = input("Bitte geben Sie Ihren Namen ein: ") # Eingabe 
print("Hallo,", name) # Ausgabe

Die input()-Funktion in Python wird verwendet, um Benutzereingaben von der Tastatur zu lesen.

  1. Lesen und Schreiben von Dateien:

# Datei zum Lesen öffnen
try:
    # Öffnen der Datei im Lesemodus ('r')
    file = open("datei.txt", "r")
    
    # Lesen der Datei
    file.read()

    print("Datei erfolgreich gelesen und geschlossen.")
except IOError: # geht das noch genauer?
    print("Fehler beim Lesen der Datei.")
finally:
    # Schließen der Datei
    file.close()

# Datei zum Schreiben öffnen
try:
    # Öffnen der Datei im Schreibmodus ('w')
    file = open("datei.txt", "w")
    
    # Schreiben in die Datei
    file.write("Dies ist ein Beispieltext.")

    print("Datei erfolgreich geschrieben und geschlossen.")
except IOError:
    print("Fehler beim Schreiben in die Datei.")
finally:
    # Schließen der Datei
    file.close()

Die open()-Funktion in Python wird verwendet, um eine Datei zum Lesen oder Schreiben zu öffnen. Durch Angabe des Dateinamens und optional des Modus (read (‘r’), write (‘w’). append (‘a’) oder read/write (‘rw’)) können Dateien geöffnet und manipuliert werden. Wichtig dabei zu beachten ist, dass die Dateien mit .close() auch wieder geschlossen werden müssen!

Das kann natürlich auch vereinfacht werden:

try:
    # Öffnen der Datei im Schreibmodus ('w') mit with
    with open("datei.txt", "w") as file:
        # Schreiben in die Datei
        file.write("Dies ist ein Beispieltext.")
    print("Datei erfolgreich geschrieben und automatisch geschlossen.")
except FileNotFoundError as fnf_error:
    print(fnf_error)
except IOError:
    print("Fehler beim Schreiben in die Datei.")

Der with-Block sorgt dafür, dass die Datei automatisch geschlossen wird, sobald der Block verlassen wird. Dies geschieht unabhängig davon, ob ein Fehler auftritt oder nicht (ähnlich um finally-Block). Dadurch wird das manuelle Schließen der Datei vermieden und der Code wird sauberer und weniger fehleranfällig. Der try-except-Block fängt potenzielle Fehler ab, die beim Öffnen oder Schreiben in die Datei auftreten könnten, und ermöglicht es dem Programm, darauf angemessen zu reagieren.

Eine Frage des Geschmacks

  1. Kommen wir zurück zu der Frage nach dem leckersten Schokoriegel aus Tag 2. Überprüfen Sie hierfür welcher Schokoriegel (Mars, Bounty, Snickers, Twixx, Milky Way) bevorzugt wird und geben Sie entsprechend eine Meinung ab. Fragen Sie dieses Mal die Meinung durch die Funktion input() direkt vom Benutzer ab und behandeln Sie potentielle Fehlerquellen in Ihrem Code. Fragen Sie so lange wieder nach, bis eine valide Anwort gegeben werden konnte. Geben Sie anschließend Ihr Programm Ihrer Nachbarin oder Ihrem Nachbarn zur Bewertung des Geschmacks.

    • (Bonus) Versuchen Sie bei der Fehlerbehandlung benutzer:Innenfreundlich vorzugehen. D.h. Wenn Fehler entstehen, die dennoch verstanden werden können veruchen Sie das Problem im Code zu lösen und keinen Fehler zu werfen.

    • Wenn Sie das Programm als Tester:In bekommen, versuchen Sie einen Fehler zu provozieren. Besprechen Sie anschließend wie Sie den Fehler bewerten, abfangen und verarbeiten können.

  1. I/O writing: Es wird Sommer! Schokoriegel sind out. Eis ist in! Sie brauchen neue Sorten und deren Bewertungen. Sie möchten allerdings nicht jeden Saisonwechsel neu anfangen und möchten sich die Sorten und Bewertungen gerne schriftlich merken.

  • Öffnen Sie ein neues eissorten.txt Dokument und schreiben Sie in die erste Zeile Sorte, Bewertung (Trennzeichen: Komma)

  • Erstellen Sie jeweils zwei Listen oder ein Dictionary (Ihre Entscheidung), mit je fünf Eissorten und deren Bewertungen.

  • Fügen Sie nun diese Informationen entsprechend der ersten Zeile dem Dokument hinzu. Bspw.: Vanille,10/10.

  1. I/O reading: Öffnen sie die eissorten.txt Datei in einem Bearbeitungsprogramm und ändern Sie die Bewertungen.

    • Öffnen und lesen Sie die Datei Linie für Linie und speichern Sie die Informationen entsprechend ab.

    • Geben Sie nun dem/der Benutzer:In die Möglichkeit die neuen Bewertungen für entsprechende Sorten abfragen zu können.

    • Bedenken Sie auch hier wieder mögliche Fehlerquellen und behandeln Sie diese entsprechend.