Tag 4 - Debugger, Klassen, Exceptions & I/O#

AI-generiertes Bild eines Debuggers

Bis heute haben wir

  • die Grundlagen zu Datentypen kennengelernt

  • wissen wie wir sie Variablen zuweisen können

  • und können Code wiederverwenden, 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, gibt es davon heute noch mehr. Wir lernen zum einen, wie diese auch automatisch verarbeitet werden können, zum anderen auch wie sie etwas weniger kryptischer erscheinen. Als allererstes schauen wir uns daher ein mächtiges Werkzeug zum besseren Nachvollziehen von Fehlerquellen an - den Debugger.

Debugger#

Ein Debugger ist ein durchaus mächtiges Werkzeug, das Programmierern hilft, Fehler (auch bekannt als Bugs) in ihrem Code zu finden und zu beheben. Das kann im ersten Moment etwas einschüchternd wirken, möchte im Kern jedoch nur helfen.

Ein Debugger führt ein Python-Dokument etwas anders 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 jedem Zeitpunkt 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, sogenannte 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 ermöglicht es, 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 alle, die Code schreiben, unabhängig von ihrem Erfahrungslevel.

Der folgende Code soll als Beispiel dienen, um erste Schritte mit dem Debugger zu gehen.

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.")
    
  2. 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?
    # Wie speichere ich die Personen mit dem errechneten Durchscnittswert zusammen ab?
    
  3. 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, folgt jetzt eine kleine Einführung zu Klassen.

Klassen in Python sind Bausteine, die es Ihnen 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 Punkt erstellt:

import math

class Punkt:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def berechne_distanz_zu(self, punkt2):
        return math.sqrt((punkt2.x - self.x) ** 2 + (punkt2.y - self.y) ** 2)

In diesem Beispiel:

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

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

    • Eine Instanz einer Klasse ist das konkrete Objekt einer Klasse. Sie können sich vorstellen, dass eine Klasse im Wesentlichen ein Bauplan oder eine Schablone ist. In diesem Fall beschreibt dieser, dass ganz generell zu einem Punkt eine X-Koordinate und eine Y-Koordinate gehören. Eine Instanz ist nun ein konkretes Objekt, welches auf dieser Schablone basiert - also der eigentliche Punkt. Von einer Klasse können so mehrere Punkte erstellt werden, die jeweils unterschiedliche Koordinaten haben können, aber alle die beiden Variablen x und y haben. 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 sind Marke und Größe immer unterschiedlich (je Fahrzeug).

  • self ist eine Referenz auf die aktuelle Instanz. Es wird bei allen Methoden benötigt und erlaubt es uns, diese als Methoden auf Objekten auszuführen.

  • self.x und self.y sind Attribute, die an jede Instanz der Klasse gebunden sind.

  • berechne_distanz_zu() ist eine Methode, die den Abstand zwischen zwei Punkten berechnet.

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

punkt1 = Punkt(1, 1)
punkt2 = Punkt(1, 3)

print(punkt1.y) # Ausgabe: 1
print(punkt2.y) # Ausgabe: 3

print(punkt1.berechne_distanz_zu(punkt2)) # Ausgabe: 2.0

punkt1.x = 2

print(punkt1.berechne_distanz_zu(punkt2)) # Neue Ausgabe ist nicht 2.0 ;)

punkt1 - punkt2 # Ausgabe: TypeError: unsupported operand type(s) for -: 'Punkt' and 'Punkt'

Vererbung#

Vererbung ist ein zentrales Konzept in der objektorientierten Programmierung. Es ermöglicht, Eigenschaften und Verhaltensweisen einer vorhandenen Klasse zu übernehmen (zu erben) 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 Die Klasse, von der sie erbt, wird als die Basisklasse oder Superklasse bezeichnet.

Um Vererbung anhand der Klassen Viereck, Rechteck, Quadrat und Raute zu erklären, nutzen wir Viereck als Basisklasse. Rechteck und Raute erben von Viereck, und Quadrat von Rechteck. Hier ist ein Beispiel, wie das aussehen könnte:

class Viereck:
    def __init__(self, p1, p2, p3, p4):
        self.p1 = p1
        self.p2 = p2
        self.p3 = p3
        self.p4 = p4

    def berechne_umfang(self):
        seiten = [
            self.p1.berechne_distanz_zu(self.p2),
            self.p2.berechne_distanz_zu(self.p3),
            self.p3.berechne_distanz_zu(self.p4),
            self.p4.berechne_distanz_zu(self.p1)
        ]
        return sum(seiten)


class Rechteck(Viereck):
    def __init__(self, p1, p2, p3, p4):
        super().__init__(p1, p2, p3, p4)

    def ist_quadrat(self):
        seite1 = self.p1.berechne_distanz_zu(self.p2)
        seite2 = self.p2.berechne_distanz_zu(self.p3)
        return seite1 == seite2

    def berechne_flaeche(self):
        seite1 = self.p1.berechne_distanz_zu(self.p2)
        seite2 = self.p2.berechne_distanz_zu(self.p3)
        return seite1 * seite2

class Quadrat(Rechteck):
    def __init__(self, p1, seitenlaenge):
        p2 = Punkt(p1.x + seitenlaenge, p1.y)
        p3 = Punkt(p1.x + seitenlaenge, p1.y + seitenlaenge)
        p4 = Punkt(p1.x, p1.y + seitenlaenge)
        super().__init__(p1, p2, p3, p4)

    def berechne_flaeche(self):
        seitenlaenge = self.p1.berechne_distanz_zu(self.p2)
        return seitenlaenge ** 2

class Raute(Viereck):
    def __init__(self, p1, p2, p3, p4):
        super().__init__(p1, p2, p3, p4)

    def berechne_flaeche(self):
        d1 = self.p1.berechne_distanz_zu(self.p3)  # Diagonale von p1 to p3
        d2 = self.p2.berechne_distanz_zu(self.p4)  # Diagonale von p2 to p4
        return (d1 * d2) / 2

In diesem Beispiel dient die Klasse Viereck als Basisklasse und enthält die grundlegende Information zu den Eckpunkten sowie eine Methode berechne_umfang(). Diese berechnet den Umfang des Vierecks. Die Klassen Rechteck und Raute erben von Viereck, was bedeutet, dass sie alle Attribute und Methoden von Viereck automatisch übernehmen. Sie können jedoch auch zusätzliche Attribute und Methoden haben, die spezifisch für Rechtecke bzw. Rauten sind. Die __init__()-Methoden der abgeleiteten Klassen rufen den Konstruktor der Basisklasse auf: super().__init__() auf. Dies stellt sicher, dass die Attribute der Basisklasse initialisiert werden. Die abgeleiteten Klassen können zusätzliche Methoden haben, die spezifisch für ihre Rolle sind. Ein Beispiel ist die Methode ist_quadrat() der Rechteck-Klasse. Es können auch Methoden mit gleichen Namen existieren, die je nach Klasse unterschiedliche Funktionalität haben, wie die Methode berechne_flaeche(). Die Hierarchie der Vererbung kann beliebig fortgesetzt werden. So vererbt die erbende Klasse Rechteck an die Klasse Quadrat. Dabei wird die berechne_flaeche()-Methode der Klasse Rechteck überschrieben.

Beispiel der Verwendung:

p1 = Punkt(0, 0)
p2 = Punkt(3, 0)
p3 = Punkt(3, 4)
p4 = Punkt(0, 4)

viereck = Viereck(p1, p2, p3, p4)
print(viereck.berechne_umfang()) #14.0

rechteck = Rechteck(p1, p2, p3, p4)
quadrat = Quadrat(p1, 3)

print(rechteck.berechne_umfang()) #14.0
print(rechteck.ist_quadrat()) #False
print(rechteck.berechne_flaeche()) #12.0
print(quadrat.berechne_flaeche()) #9.0
print(quadrat.ist_quadrat()) #True

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

Quatsch mit Klassen

  1. Bereits am ersten Tag haben wir die verschiedenen Arten von Operatoren besprochen, die auf grundlegende Datentypen angewandt werden können. Auf die Klasse Punkt können diese Operatoren aktuell noch nicht angewandt werden. Glücklicherweise gibt es in Python die Möglichkeit, Operatoren zu überschreiben. Fügen Sie zunächste eine Methode __str__() zur Punkt-Klasse hinzu. Diese soll einen String zurückgeben, der die wichtigen Informationen des Punktes enthält. Testen Sie Ihre Methode, in dem sie ein Objekt der Punkt-Klasse initialisieren und auf dieses die Funktion str() anwenden.

  2. Erstellen Sie im nächsten Schritt eine Möglichkeit, zwei Punkte miteinander zu vergleichen. Dafür müssen Sie eine Methode __eq__() erstellen, die als Parameter eine weitere Instanz der Klasse Punkt nimmt. In dieser werden die relevanten Informationen des Punktes verglichen und schließlich einen entsprechender bool zurückgegeben. Testen Sie ihre Funktion indem Sie auf zwei Punkte den ==-Operator anwenden.

  3. Auch arithmetische Operatoren können überschrieben werden. Erstellen Sie eine Funktion __add__(), die einen weitere Parameter annimmt. In dieser Funktion können Sie nun, je nachdem ob der weitere Parameter eine Punkt oder eine Skalar (int, float) ist, die Attribute des ursprünglichen Punktes verändern. Überprüfen Sie ihre Funktion, indem sie auf zwei Punkte den +-Operator anwenden.

  4. Die Klassen beinhalten aktuell keine Validierung der Eingabepunkte. Fügen Sie einfache Validierungen hinzu und überlegen Sie, für zu welcher Klasse diese am sinnvollsten hinzugefügt werden kann, um Wiederverwendbarkeit zu fördern. Überprüfen Sie, dass keine Punkte mehrmals vorkommen. Bei rauten und Quadraten sollten alle Seiten gleich lang sein. Beim Rechteck sollten die gegenüberliegenden Seiten gleich lang sein. Wenn Ihnen weitere Regeln einfallen, fügen Sie diese gerne auch hinzu.

  5. Bisher haben sich die alle Objekte auf den zweidiemnsionalen Raum beschränkt. Erstellen Sie nun eine neue Klasse 3DPunkt, der von der Punktklasse erbt. Dieser soll ein zusätzliches Attribut für den z-Wert haben und die Methoden entsprechend angepasst werden.

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. Das Ziel ist, dass 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’). Weil potentiell (oder hier: absichtlich) ein Fehler auftritt, wird das Statement in einem try-Block ausgeführt. Der except-Block fängt diesen achtlos hingeworfenen Error auf und führt ein passendes print()-Statement 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 behandelt.

Zweites Beispiel: Wichtig zu wissen ist, dass wie bspw. bei if-else-Blöcken, excepts von oben nach unten getestet werden.

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 im Kontext dieses Kurses nicht ein. 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 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 war im vierten Beispiel zu sehen. Hier wird der Fehler im except-Block der Exception gefangen und verarbeitet. Das ist soweit 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. Diese sagt uns detailliert, 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 zu einem ZeroDivisionError kommt, ist mit Sicherheit schon an einer anderen Stelle etwas schief gelaufen. Das Programm sollte beendet, repariert und neu gestartet werden. Denken Sie daran - Fehlermeldungen sind Hilfestellungen und keine Rüge!

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

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.

  2. Sie bekommen einen AttributeError während Sie die Koordinaten eines Punktes von oben abfragen wollten. Warum? Gehen Sie entsprechend mit diesem Fehler um.

    punkt1 = Punkt(1, 1)
    punkt1.links_unten()
    
    
    '''
    Ausgabe:
    AttributeError: 'Punkt' object has no attribute 'links_unten'
    '''
    

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 vieles handeln:

  • das Lesen von Daten aus einer Datei

  • die Eingabe durch die Nutzerin über die Tastatur oder Maus

  • das Senden von Daten über das Netzwerk

  • das Anzeigen von Informationen auf dem Bildschirm

Jetzt behandeln wir nur das eigenständige Eingeben durch eine Nutzerin 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 analog zu 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. Nach Norden auf die Mittermaier Straße, über die Brücke, an der Shell-Tankstelle links. Ein relativer Pfad würde im Gegensatz dazu von Ihrem momentanen Standort im Hörsall aus beginnen. Aus dem Gebäude raus, dann links zur Shell-Tankstelle. Dort rechts. Beide Arten von Pfaden haben ihre Vor- und Nachteile.

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

Interaktion mit der Nutzerin über die Konsole#

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

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

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 zum Glück 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 wie beim 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, 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 von der Nutzerin 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.

    • Versuchen Sie bei der Fehlerbehandlung benutzer:Innenfreundlich vorzugehen: Wenn Fehler entstehen, die dennoch verstanden werden können, versuchen 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.

  2. 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 programmatisch entsprechend der ersten Zeile Ihrem Dokument hinzu, zum Beispiel: Vanille,10/10.

  3. Öffnen sie die eissorten.txt Datei in einem Texteditor 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 der Nutzerin 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.