# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Inhalt
#
# Im sechsten Kapitel behandeln wir die verbleibenden Themen zur Beschreibung der Programmiersprache Python und decken die folgenden Konzepte ab. 
# - Exceptions
# - Dateioperationen
# - Programmierstil
#
# # Exceptions
#
# Es gab bisher viele Beispiele von Fehlermeldungen, zum Beispiel beim Zugriff auf Variablen, die nicht existieren.

undefined_variable

# Im obigen Beispiel gibt es die Fehlermeldung ```NameError: name 'undefined_variable' is not defined```. Bei dieser Fehlermeldung handelt es sich um eine sogenannte *Exception* from Typ ```NameError```. Bisher haben wir immer so getan, als müssten Exceptions zwangsweise dazu führen, dass die Ausführung von Programmen abgebrochen wird. Das stimmt jedoch nicht. Das besondere am Konzept von Exceptions als Fehlermeldung ist, das man sie *behandeln* kann und sie dann nicht mehr zwingend zum Abbruch der Ausführung eines Programms führen. Hierzu gibt es die Schlüsselwörter ```try``` und ```except```. Mit ```try``` beginnt man einen Block, in dem möglicherweise eine Exception erwartet wird. Tritt innerhalb des ```try``` Blocks eine Exception auf bricht das Programm nicht einfach ab. Stattdessen wird der ```except``` Block ausgeführt. Tritt keine Exception auf, wird der ```except``` Block ignoriert. 

# +
try:
    undefined_variable                          # NameError -> go to except
    print("undefined_variable is defined")
except:
    print("undefined_variable is not defined")
    
defined_variable = 42
try:
    defined_variable                            # no exception -> continue execution and ignore except block
    print("defined_variable is defined")
except:
    print("defined_variable is not defined")
# -

# Im obigen Beispiel fängt man beliebige Exceptions ab, also nicht nur Exceptions vom Typ ```NameError```, wie wir eigentlich erwarten, sondern auch alle andern. Man kann einschränken, welche Typen von Exception behandelt werden, in dem man die erwarteten Typen bei der ```except``` Anweisung angibt.

try:
    undefined_variable
    print("undefined_variable is defined")
except NameError:                               # only handles NameError
    print("undefined_variable is not defined")    

# Tritt eine anderer Typ von Ausnahme auf, wird dieser nicht behandelt. 

try:
    1/0            # division by zero not allowed -> ZeroDivisionError
except NameError:  # only handles NameError but not ZeroDivisionError
    print("there was a NameError")

# Für den Fall, dass man erwartet das verschiedene Typen von Exceptions auftreten könnten, kann man diese sowohl in einem ```except``` behandeln, als auch durch mehrere ```except``` Blöcke.

# +
try:
    1/0
except (NameError, ZeroDivisionError):
    print("there was a NameError or a ZeroDivisionError")
    
try:
    1/0
except NameError:
    print("there was a NameError")
except ZeroDivisionError: 
    print("there was a ZeroDivisionError")


# -

# # Built-in Exceptions
#
# Die am häufigsten vorkommenden Typen von Ausnahmen sind die *built-in* Exceptions, die im Sprachstandard in Python direkt mitgeliefert werden. ```NameError``` und ```ZeroDivisionError``` haben wir bereits kennengelernt. Weitere Beispiele für Exception sind:
# - ```Exception``` als Basisklasse für Exceptions.
# - ```ArithmeticError``` als Basisklasse für Exceptions in arithmetrischen Operationen (z.B. ```ZeroDivisionError```).
# - ```AttributeError```  als Gegenstück zum ```NameError``` wenn es Probleme mit Namen in Klassen gibt.
# - ```ModuleNotFoundError``` wenn ein Modul nicht im Modulpfad gefunden werden kann.
# - ```KeyError``` wenn ein gesuchter Schlüssel in einem ```dict``` nicht existiert.
# - ```IndexError``` bei Zugriffen auf Listen oder ähnlichen Datentypen außerhalb des validen Bereichs.
# - ```TypeError``` wenn eine Funktion mit einem Objekt aufgerufen wird, welches vom Typ her nicht unterstützt wird (falsche Klasse). 
# - ```ValueError``` wenn eine Funktion mit falschen Werten aufgerufen wird, aber der Typ der Objekte korrekt ist.
#
# Die vollständige Liste der built-in Exceptions ist in der [Python Dokumentation](https://docs.python.org/3/library/exceptions.html#bltin-exceptions) verfügbar. 
#
# # Erzeugen von Exceptions
#
# Bisher haben wir uns nur angeguckt, wie man Exceptions behandelt. Jetzt wollen wir uns angucken, wie man selbst Exceptions erzeugt. Das erzeugen von Exceptions wird auch *werfen* genannt.  Als Beispiel für das Werfen von Exceptions nutzen wir wieder unsere Wurzelfunktion. Außerdem verdeutlichen wir den Unterschied zwischen dem ```TypeError``` und dem ```ValueError```.

def my_sqrt(x):
    """Returns the square root of x"""
    guess = 1
    while(abs(guess*guess-x)>0.0001):
        guess = (1/2)*(guess+x/guess)
    return guess


# Bisher haben wir die Funktion immer mit dem korrekten Typ von Objekt aufgerufen, also mit Zahlen. In der Funktion selbst wird jedoch nirgends überprüft, ob es wirklich eine Zahl ist. Was bei dem Aufruf mit einer Zeichenkette passiert, ist daher nicht direkt offensichtlich. 

my_sqrt("42")


# Wir bekommen als einen ```TypeError```, da die Substraktion von Zahlen und Zeichenketten nicht definiert ist. Positiv hierbei ist, das der Typ der Exception, die geworfen wird, richtig ist, da Zeichenketten als Typ ja nicht unterstützt werden sollten. Die Fehlermeldung selbst ist jedoch sehr unglücklich. Statt der klaren Aussage, dass die Quadratwurzel für Zeichenketten nicht definiert ist, bekommt man eine konkrete Division in der Funktion als Problem gemeldet. Um eine bessere Fehlermeldung zu liefern, können wir selbst am Anfang der Methode überprüfen, ob es sich um eine Zahl handelt. Falls dies nicht der Fall ist, können wir mit Hilfe des Schlüsselworts ```raise``` selbst einen ```TypeError``` mit einer besseren Fehlermeldung generieren. 

# +
def my_sqrt_exceptions(x):
    """Returns the square root of x"""
    if not (isinstance(x, float) or isinstance(x, int)): # checks type of parameter with isinstance
        raise TypeError("Only supports int and float")   # raise TypeError is not numeric
    guess = 1
    while(abs(guess*guess-x)>0.0001):
        guess = (1/2)*(guess+x/guess)
    return guess

my_sqrt_exceptions("42")


# -

# Die nächste Eigenschaft der Quadratwurzel, die wir bisher ignoriert haben, ist die Tatsache, dass die Wurzel von negativen Zahlen im reellen undefiniert ist. Dieses Eigenschaft spiegelt sich aber nicht in unserer Funktion wieder. Führt man die Funktion mit negativen Zahlen aus, landet man in der Regel in einer Endlosschleife. Daher sollte man vor dem Ausführen des Heronverfahrens überprüfen, ob die Zahl positiv ist. Wenn dies nicht der Fall ist, kann man mit einem ```ValueError``` auf den ungültigen Eingabewert (der korrekten Klasse) reagieren. 

# +
def my_sqrt_exceptions(x):
    """Returns the square root of x"""
    if not (isinstance(x, float) or isinstance(x, int)):
        raise TypeError("Only supports int and float")
    if x<0:
        raise ValueError("Square root only defined for positive numbers") # raise ValueError for invalid input values
    guess = 1
    while(abs(guess*guess-x)>0.0001):
        guess = (1/2)*(guess+x/guess)
    return guess

my_sqrt_exceptions(-42)


# -

# # Benutzerdefinierte Exceptions
#
# Es gibt Fälle, wo man für eine Anwendung eigene Ausnahmen definieren möchte, zum Beispiel weil keine built-in Exception passend ist. Hierfür kann man eigene Exceptions definieren, in dem man eine neue Klassen erstellt, die von der Klasse ```Exception``` abgeleitet ist.

# +
class MyException(Exception):
    pass # empty class block requires pass

raise MyException("My very own Exception")


# -

# Man muss auch nicht direkt von ```Exception``` Ableiten. Wenn man zum Beispiel eine Bibliothek mit mathematischen Funktionen entwickelt, kann es zum Beispiel auch Sinn machen ```ArithmeticException``` als Basisklasse zur nehmen. 

# # Hierarchien zwischen Exceptions
#
# Exceptions sind durch Vererbung als Klassenhierarchie organisiert. Dies ist für die Auswertung der ```except``` Anweisung wichtig: Es wird nicht nur genau die definierte Klasse behandelt, sondern auch alle ihre Kindklassen. Hierdurch kann es auch mehrere ```except``` Anweisungen geben, die auf die selbe Klasse zutreffen, weil verschiedene Hierarchieebenen der gleichen Klasse abgefangen werden. In diesem Fall zählt die Reihenfolge der ```except``` Anweisungen. Zur Veranschaulichung brauchen wir zuerst eine einfache Hierarchie. 

# +
class ParentException(Exception):
    pass

class ChildException(ParentException):
    pass


# -

# Wenn wir nun die ChildException werfen und es ```except``` Anweisungen sowohl für die ```ParentException``` als auch für die ```ChildException``` gibt, hängt das Verhalten von der Reihenfolge der Klassen im ```except``` ab. Behandelt man zuerst die ```ParentException``` wird man nie im Block der ```ChildException``` landen können.

try:
    raise ChildException()
except ParentException:
    print("except for ParentException")
except ChildException:
    print("except for ChildException")

# Ändert man die Reihenfolge, wird die ```ChildException``` auch vom ```except``` Block der ```ChildException``` behandelt. 

try:
    raise ChildException()
except ChildException:
    print("except for ChildException")
except ParentException:
    print("except for ParentException")

# # finally and with
#
# Es gibt Fälle, da müssen Anweisungen auf jeden Fall ausgeführt werden, egal ob eine Exception geworfen wird oder nicht. Solche Anweisungen sind häufig zum Aufräumen da: Freigabe von Resourcen, sicherstellen das Daten konsistent sind, und ähnliches. Hierfür gibt es das Schlüsselwort ```finally```. Hiermit lässt sich ein Block definieren, der immer im Anschluss an einen ```try``` Block ausgeführt wird. Selbst wenn eine Ausgabe nicht behandelt wird, wird zuerst der ```finally``` Block ausgeführt, bevor die Exception die Programmausführung beendet.

# case 1: finally after an exception was handled
try:
    undefined_variable
except NameError:
    print("except")    
finally:
    print("finally block")

# case 2: finally after leaving try without exception
try:
    pass
except NameError:
    print("except")    
finally:
    print("finally block")

# case 3: finally with unhandled exception
try:
    1/0
except NameError:
    print("except")    
finally:
    print("finally block")

# Es gibt auch Objekte, die ihr Aufräumverhalten direkt mitliefern. Dies kann man über das Schlüsselwort ```with``` nutzen. Als Beispiel hierfür machen wir einen kleinen Exkurs und gucken uns die Operationen zum Lesen und Schreiben von Dateien in Python an.  

# ## Exkurs: Dateien Lesen und Schreiben
#
# Zugriff auf Dateien ist ein typisches Beispiel für Operationen, bei denen man sicherstellen muss, dass am Ende aufgeräumt wird. Werden Dateien nicht richtig geschlossen, kann dies unerwünschte Nebenwirkungen haben. Zum Beispiel kann man eventuell die Datei von einem anderen Programm aus nicht mehr schreiben, oder es werden nicht alle Daten sauber geschrieben, so dass es zum Datenverlust kommt. Wenn man Dateien mit Hilfe von ```with``` öffnet, wird garantiert das die Datei am Ende des ```with``` Blocks sauber geschlossen wird. Zum Öffnen von Dateien gibt es in Python die Funktion ```open()```. Inhalte können mit ```read``` gelesen werden und mit ```write``` geschrieben werden. Mit ```readlines``` kann man eine Datei zeilenweise lesen.

# +
# example 1: copy a file
with open('../examples/sqrt.py', 'r') as input_file: # opens file read-only in the variable file
    content = input_file.read()
    
print(content)

with open('../examples/sqrt-copy.py','w') as output_file: # opens file for writing
    output_file.write(content)
# -

# %cat ../examples/sqrt-copy.py

# example 2: print a file with line numbers
with open('../examples/sqrt.py', 'r') as input_file: # opens file read-only in the variable file
    for i, line in enumerate(input_file.readlines()):
        print(f"{i+1:3d} {line.rstrip()}") # rstrip removes whitespaces on the right side of a string


# # Regeln zum Programmierstil
#
# Die Bennung von Variablen, Funktionen, Klassen, und Modulen, sowie die Formatierung des Quelltextes sind sehr wichtig für die Lesbarkeit von Programmen durch Menschen. Betrachten Sie zur Veranschaulichung den folgenden Quelltext. 

def f(x):
 y=1
 while(abs(y *  y   -   x)   >0.0001    ):
                                                   y = (1/2)    *                                               (y+x/y)
 return y

# Der Quelltext ist syntaktisch korrekt. Inhaltlich ist er identisch zur Funktion ```my_sqrt```. Der Quelltext ist jedoch viel schwerer zu lesen. Fragen Sie sich einfach, ob Sie verstanden hätten, was diese Funktion tut, wenn Sie nicht zufällig bereits die Wurzelfunktion kennen würden. Es gibt zwei offentliche Gründe, warum die obige Funktion schwerer zu lesen ist: 
# - Die Bennung der Variablen, insbesondere auch der Funktion, hat keinen selbsterklärenden Zusammenhang zu ihrer Bedeutung mehr. 
# - Die Formatierung ist nicht Einheitlich. Dies gilt sowohl für die Einrückung, als auch für die Leerzeichen innerhalb der Anweisungen. 
#
# Natürlich ist die obige Zelle überspitzt dargestellt. Aber insbesondere zu kurze Namen sind ein häufig verbreitetes Problem. Für Python gibt es mit [PEP 8](https://www.python.org/dev/peps/pep-0008/) einen offiziellen *Style Guide*, in dem Richtlinien für den Programmierstil festgelegt werden. Es lohnt sich für jeden Entwickler von Python einen Blick in dieses Dokument zu werfen. Hier sind nur einige Auszüge: 
# - Blöcke sollten immer mit 4 Zeichen eingerückt sein. 
# - Die Namen von Variablen, Funktionen, Module und Packages sollten nur aus kleinen Buchstaben bestehen. Unterstriche können verwendet werden, wenn sie die Lesbarkeit erhöhen. Die Namen von Modulen und Packages sollten möglichst kurz sein. 
# - Klassennamen sollten CapWords (auch CamelCasing genannt) genannt verwenden. 
# - Konstanten (Variablen, die nie verändert werden sollten) sollten nur aus Großbuchstaben bestehen. Unterstriche können verwendet werden, wenn sie die Lesbarkeit erhöhen. 

StarcoderdataJupyterScriptsDedupFiltered