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

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?
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.")
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?
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()
Bonusaufgabe: Eine Ebene tiefer
Was macht die Funktion rnd.randint()
? Gehen Sie dafür ins REPL und geben Sie import random as rnd
und anschließend rnd
ein. Öffnen Sie sie Ihren Datei-Explorer im angezeigten Pfad und öffnen Sie die Datei random.py
. In dieser Datei suchen Sie die Funktion randint()
. Was passiert hier?
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 KlassePunkt
.Die Methode
__init__()
ist ein Konstruktor, der aufgerufen wird, wenn eine Instanz der Klasse erstellt wird. Hier werden die Attributex
undy
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
undy
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
undself.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
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 Funktionstr()
anwenden.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 KlassePunkt
nimmt. In dieser werden die relevanten Informationen des Punktes verglichen und schließlich einen entsprechenderbool
zurückgegeben. Testen Sie ihre Funktion indem Sie auf zwei Punkte den==
-Operator anwenden.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 einePunkt
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.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.
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, except
s 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!
Bonusinhalt: Fehler selber werfen
Exceptions können durch das Schlüsselwort raise
auch selbst geworfen werden.
Das Schlüsselwort assert
ermöglicht es ähnlich 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
Bonusinhalt: Fehler, die nicht abgefangen werden
Es gibt Fehler wie den SyntaxError
, die vor der Ausführung des eigentlichen Programmes erkannt und geworfen werden.
Diese können durch einen try
-except
-Block nicht aufgefangen werden und werden immer geworfen.
Das ist auch wünschenswert, 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
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.
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' '''
Bonusinhalt: Subklassen nicht implementiert
Betrachten Sie 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 passiert hier? 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 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.
Bonusinhalt: Na was denn nun? OS oder IOError?
Eventuell ist Ihnen aufgefallen, dass in den oberen Code-Beispielen der IOError
geworfen wird, im Baumdiagramm jedoch ein OSError
an seiner Stelle steht.
Eine Antwort warum finden Sie hier auf Stackoverflow.
Stackoverflow ist eine generell sehr gute Informationsquelle und ein guter Ort zum Austausch.
Das Schwarmwissen kann häufig sogar die absurdesten Dinge beantworten.
Eine Frage des Geschmacks
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.
Anmerkung zu potentiellen Fehlern
Denken Sie an Groß- und Kleinschreibung. Wie kann hier Nutzer:Innen-freundlich vorgegangen werden?
Was macht ihr Programm, wenn Sie ein Leerzeichen zu viel eingeben?
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 ZeileSorte, 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
.
Ö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.
Hilfestellungen
Welchen Modus brauchen Sie zum Lesen einer Datei?
Zeilenumbrüche in Textdokumenten werden mit
\n
dargestellt. Die Funktion.strip()
könnte einen Blick wert sein.Schauen Sie sich die Funktionalitäten der Funktion
.readlines()
an. Eine ausführliche Erklärung finde Sie hier. Was wird von der Funktion an die Variable zurückgegeben?