Python 3 Crashkurs

27.02.2018, 12:41 - Autor: PGD

Warum Python?

Im Grund ist Python eine sehr einfach zu lernende Sprache. Die folgenden Punkte beschreiben den Ansatz von Python sehr gut:


All das macht Python so beliebt - mit wenigen Zeilen Code können Sie in Windesweile kleine Tools schreiben.

Für die folgenden Beispiele verwende ich die Python-IDLE. Wenn wir diese Starten erhalten wir eine interaktive Python-Shell mit der wir im folgenden Arbeiten werden.

user@kali:~$ python3
Python 3.6.3 (default, Jan 19 2017, 14:11:04) 
[GCC 6.3.0 20170118] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

Datentypen & Variablen

a) None

Der Datentyp None kennzeichnet eine Variable als nicht gesetzt und wird wie folgt angelegt:

>>> n = None

Eine Variable ist quasi ein Wert im Speicher, der über einen Namen angesprochen werden kann. Eine Variable wird mit dem Schema [NAME] = [WERT] angelegt.

>>> 2var = 2
SyntaxError: invalid syntax

Der Name darf die Zeichen A-Z, a-z, _ und 0-9 enthalten und muss mit einem _ oder Buchstaben beginnen.

>>> var2 = 2
>>> print(var2)
2
>>> print(Var2)
Traceback (most recent call last):
  File "", line 1, in 
    Var2
NameError: name 'Var2' is not defined

Variablennamen sind Case-sensitive, sprich es wird zwischen Groß- und Kleinschreibung unterschieden. Daher meldet der Python-Interpreter an dieser Stelle auch, dass Var2 nicht definiert ist.

>>> var2 = None

Außerdem kann man so eine Variable bewusst wieder auf den Status undefiniert setzen um einen Wert zu löschen.

>>> var2 = None
>>> 2 * var2
Traceback (most recent call last):
  File "", line 1, in 
TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'
>>> var2 = 0
>>> 2 * var2
0

Das macht vor allem dann Sinn wenn mit der Variable gerechnet werden soll und wir verhindern wollen, dass versehentlich mit falschen Rechenergebnissen weiter gearbeitet wird.

Ist der Wert von var2 auf None gesetzt dann bricht das Programm die Berechnung 2 * var2 mit einer Fehlermeldung ab. Der Type-Error erklärt uns kurz und knapp, dass int-Werte nicht mit None-Werten multipliziert werden können.

Hätten wir zum resetten der Variable einfach nur 0 verwendet würde die Berechnung durchgeführt und das Ergebnis wäre logischer Weise dann auch wieder 0.

b) int

Integer-Werte (kurz int) sind Ganzzahlen.

>>> a = 1
>>> b = 0xF
>>> a + b
16
>>> c = 0b10
>>> a + c
3

Hierbei ist die dezimale, hexadezimale (zB 0xF) und binäre (zB 0b10) Schreibweise erlaubt. Das hexadezimale Zahlensystem basiert nicht auf 10 sondern auf 16. Hierbei werden die Buchstaben A-F verwendet um die zahlen 10-15 darzustellen. Somit entspricht 0xF der Dezimalzahl 15. Binärzahlen basieren auf 2 und daher bedeute 0b10 ein 2er und null 1er also die Dezamalzahl 2.

c) float

Float sind Fließkommazahlen. Diese Werte werden in der englischen Schreibweise (Punkt statt Komma) angelegt.

>>> a = 1.5
>>> b = 2
>>> a + b
3.5
>>> a + a
3.0
>>> a * 2
3.0
>>> b / 2
1.0

Natürlich kann man int- und float-Werte in einer Berechnung mischen. Wenn ein float-Wert in der Berechnung vorkommt ist das Ergebnis wieder ein float-Wert unabhängig davon, ob das Ergebnis eine Ganzzahl wäre oder nicht!

Eine Division liefert immer einen float-Wert unabhängig davon, ob das Ergebnis eine Ganzzahl wäre oder nicht!

d) String

Zeichenketten (auch Strings genannt) sind

>>> a = hallo
Traceback (most recent call last):
  File "", line 1, in 
NameError: name 'hallo' is not defined

Ohne Anführungzeichen wird das Wort hallo als Variablenname interpretiert und versucht der Variablen a den Wert von der Variablen hallo zuzuweisen. Da diese nicht existiert tritt der NameError auf.

>>> a = "hallo"
>>> b = 'welt'
>>> a + " " + b
'hallo welt'

Die Zeichenkette kann in einfachen oder doppelten Anführungzeichen eingeschlossen werden. Darüber hinaus kann man Strings mit dem + Operator zusammenfügen.

>>> csv = "Max Muster,Musterstr. 1,1010,Wien,01/123456"
>>> csv.split(",")
['Max Muster', 'Musterstr. 1', '1010', 'Wien', '01/123456']

Alles in Python ist ein Objekt - daher kann auch auf einem String eine sogenannte Methode angewandt werden. Hier in diesem Beispiel trennen wir eine CSV-Zeile mit split() an den Beistrichen auf und erzeugen daraus eine Liste. Natürlich gibt es viele weitere Methoden die auf einen String angewendet werden können.

e) Listen

Listen sind eine Ansammlung von Werten, die zusammen unter dem gleichen Namen angesprochen werden können. Dies kann Sinn machen wenn die Werte zB zu einem Datensatz gehören. Auf die Einzel-Werte wird dann mit einer Index-Zahl zugegriffen.

>>> l = [1, 2.3, "abc", None]
In Listen können auch verschiedene Datentypen gemischt werden.

>>> l.append(4)
>>> print(l)
[1, 2.3, 'abc', None, 4]

Mit append() lassen sich Werte an die Liste anhängen.

>>> l.append([5,6,7])
>>> print(l)
[1, 2.3, 'abc', None, 4, [5, 6, 7]]

Listen können außerdem verschachtelt werden.

>>> print(l[0])
1
>>> print(l[2])
abc
>>> print(l[5][0])
5
>>> print(l[5][2])
7

Der Zugriff erfolgt wie gesagt mit einer Index-Zahl. Listen-Elemente werden beginnend bei der 0 fortlaufend nummeriert. Somit greifen wir mit l[0] auf das erste und mit l[2] auf das dritte Element der Liste zu.

Bei diesem Beispiel ist l[5] (das sechste Element) wiederum eine Liste - daher spricht l[5] die ganze Liste an und l[5][0] beispielsweise das erste Element dieser Liste. Elemente können dann mit l[5].append(...) angehängt werden.

>>> print(len(l))
6

Mit len() lässt sich die Länge bzw. Anzahl der Elemente einer Liste oder vieler anderer Datentypen ermitteln. Bei einem String wird zB die Zeichenanzahl geliefert.

>>> print(l[1:3])
[2.3, 'abc']

Mit der Schreibweise 1:3 als Index wird ein sogenanntes Slicing durchgeführt - damit wird ein Teil einer Liste oder Zeichenkette extrahiert. Hier bedeutet 1:3 eine Teilliste vom zweiten Element (Index 1) bis exklusive dem vierten Element (Index 3). Daher werden die Elemente mit dem Index 1 und 2 zurückgeliefert.

>>> l.pop()
[5, 6, 7]

... liefert das letzte Element der Liste zurück und entfernt es auch gleich aus der Liste. So können Listen auch als LIFO-Stapel (Last In First Out) verwendet werden.

>>> l.pop(0)
1

Analog dazu verhält sich pop(0) bei dem ersten Listenelement und erlaubt somit die Verwendung der Liste als FIFO-Stapel (First in First Out).

f) Tupel

Tupel sind deutlich performanter als Listen. Dafür sind Sie aber deutlich weniger flexibel. Das nachträgliche Einfügen oder Löschen von Elementen ist nicht erlaubt.

>>> t = (1, 2.3, (4, 5, 6), "bla")
>>> print(t)
(1, 2.3, (4, 5, 6), 'bla')

Ein Tupel wird mit ( und ) gebildet. Auch hier ist das mischen verschiedenster Datentypen und das Verschachteln erlaubt.

>>> print(t[0])
1
>>> print(t[1:3])
(2.3, (4, 5, 6))

Der Zugriff erfolgt wie bei Listen über die Indexzahl und auch Slicing ist möglich.

>>> print(t[-1])
bla
>>> t = t[0:-1]
>>> print(t)
(1, 2.3, (4, 5, 6))

Im Grunde ist Slicing auch eine Möglichkeit mit ein Tupel zu manipulieren. Mit t[-1] wird das letzte Element angesprochen und t[0:-1] liefert alle bis auf das letzte Element. Überschreibt man den Tupel mit dem Slice so wie hier gezeigt hat man das letzte Element entfernt.

Mit zwei Slicing-Operationen haben wir quasi die pop()-Methode nachgebaut.

g) Set

Ein Set ist eine besondere Form einer Liste. Sie ist performant wie ein Tupel und hat die Eigenart, dass jeder Wert in der Liste nur einmalig vorkommen darf.

>>> s = {"a", "c", "d"}

Gebildet wird ein Set mit den geschweiften Klammern. Ein leeres Set wird mit s = set() angelegt da die geschweiften Klammern auch für Dictionaries verwendet werden. s = {} würde hingegen ein leeres Dictionary erzeugen.
>>> s.add("a")
>>> s.add("d")
>>> s.add("b")
>>> print(s)
{'a', 'c', 'd', 'b'}

Da a und d schon im Set s vorhanden sind werden diese Zahlen nicht noch einmal hinzugefügt. Die Aufrufe von s.add("a") bzw. s.add("d") werden kommentarlos ignoriert.

>>> s.add("g")
>>> s.add("f")
>>> print(s)
{'a', 'c', 'd', 'b', 'f', 'g'}

Das Hinzufügen von g und f klappt wiederum. Wobei die Elemente nicht unbedingt in der Reihenfolge in der Sie hinzugefügt wurden im Set landen. Diese Eigenarten des Sets gilt es bei der Verwendung immer zu bedenken um Fehler zu vermeiden. Im Grunde ist dieser Datensatz lediglich für einige spezielle Anwendungen sinnvoll - dort aber erspart er dem Entwickler einiges an Arbeit!

h) Dictionaries

Wir haben bis dato immer mit einer Index-Ziffer auf die Werte zugegriffen. Bei einfachen Strukturen mag das noch weniger Problematisch sein aber wie sieht es hier aus:

>>> saison = ["01.01.2018-31.03.2018", "01.04.2018-31.12.2018"]
>>> zimmer = ["Standard", "Suite"]
>>> hotels = ["Ibis", "Hilton"]
>>> preise = [[[79, 89], [179, 189]], [[249, 299], [449, 499]]]

Sehen wir uns die folgende Preisliste an dann fällt uns auf, dass wir die Preisliste nicht eindeutig lesen können ohne die Struktur der Daten zu kennen und selbst wenn wir wüssten, dass der Aufbau PREIS[Hotel][Zimmer][Saison] ist, ist die Preisliste nicht gerade einfach zu lesen. Oder können Sie adhoc sagen was eine Suite im Ibis in der Saison 01.04.-31.12.2018 kostet?

Darüber hinaus benötigen wir 4 Listen um alle Daten abbilden zu können.

Genau darum gibt es Dictionaries - Sehen wir uns die gleiche Preiliste als Dictionary an:

>>> d = {
...   "Ibis": {
...     "Standard": { "01.01.2018-31.03.2018":  79, "01.04.2018-31.12.2018":  89 },
...     "Suite":    { "01.01.2018-31.03.2018": 179, "01.04.2018-31.12.2018": 189 }
...   },
...   "Hilton": {
...     "Standard": { "01.01.2018-31.03.2018": 249, "01.04.2018-31.12.2018": 299 },
...     "Suite":    { "01.01.2018-31.03.2018": 449, "01.04.2018-31.12.2018": 499 }
...   }
... }

Deutlich übersichtlicher! Aber auch die Abfragen können wir deutlich lesbarer formulieren:

>>> print(d['Ibis']['Suite']['01.04.2018-31.12.2018'])
189

Damit können wir also festhalten, dass dieser Datentyp dazu dient zwei Werte miteinander in Beziehung zu setzen und dadurch ganz nebenbei die Lesbarkeit bei komplexeren Strukturen enorm steigert.

Der generelle Aufbau ist: {Schlüssel1 : Wert1, Schlüssel2 : Wert2}

Hierbei können Schlüssel entweder Zahlen oder Zeichenketten sein. Ein Schlüssel muss in seiner Ebene allerdings eindeutig sein:

>>> d2 = {"a" : "text1", "b" : "text2", "a" : "text3"}
>>> print(d2)
{'a': 'text3', 'b': 'text2'}

Auch hier werden wir nicht gewarnt wenn wir einen Schlüssel doppelt vergeben. Der Wert wird einfach kommentarlos überschrieben!

i) Boolean

Wahrheitswerte können nur die zwei Werte True (wahr) oder False (falsch) annehmen.

>>> password_found = False

Meist werden Sie verwendet um einen Zustand zu beschreiben oder

>>> 3 > 5
False
>>> 3 < 5
True

um den Wahrheitswert von Vergleichen zu ermitteln.

Operatoren

a) Mathematische Operatoren

>>> # Addition
>>> print(1 + 2) 
3 
>>> # Subtraktion
>>> print(4 - 3) 
1
>>> # Mulpiplikation
>>> print(5 * 6) 
30 
>>> # Division
>>> print(7 / 8) 
0.875 

Soweit sollten die Ausgaben der Grundrechnungsarten nicht besonders verblüffen. Natürlich kann man nicht nur mit selbst eingegebenen Zahlen sondern auch mit den Werten von Variablen rechnen.

>>> # 10 geteilt durch 3 ergibt 3 Rest 1
>>> # Ganzzahldivision
>>> print(10 // 3) 
3 
>>> # Modulo 
>>> print(10 % 3)
1 

Das %-Zeichen ist der Modulo-Operator. Dieser liefert den Rest der Division. Die Ganzzahldivision arbeitet wie der Modulo-Operator nur wird hier das Ergebnis und nicht der Rest geliefert.

>>> # 3 mal 1 ist 3 plus 1 ergibt 4
>>> print(1 + 1 * 3) 
4 
>>> # 1 plus 1 ist 2 mal 3 ergibt 6
>>> print((1 + 1) * 3)
6 

Auch in Python gilt Punkt- vor Strichrechnung. Wenn wir von dieser Regel abweichen müssen oder wollen, dann ist das mit einer entsprechenden Klammerung möglich. Die Berechnungen in der Klammer werden immer zuerst ausgeführt.

>>> h = "Hallo"
>>> w = "Welt"
>>> print(h + " " + w) 
Hallo Welt

Der + Operator kommt auch im Verbindung mit Texten zum Einsatz. In diesem Fall werden Texte aneinandergereiht. Wir müssen uns unbedingt merken, dass Operatoren abhängig von den Datentypen anders arbeiten.

>>> # 10 hoch 3 ist 1000
>>> print(10 ** 3)
1000

Die Exponentiation multipliziert die erste Zahl mit sich selbst. Die Anzahl dieser Multiplikationen wird mit der zweiten Zahl festgelegt.

b) Bitweise Operatoren

>>> a = 10
>>> b = 6
>>> # Bitweise Und-Verknüpfung AND
>>> print(a & b) 
2 
>>> # Bitweise Oder-Verknüpfung OR
>>> print(a | b) 
14
>>> # Bitweise Exklusiv-Oder-Verknüpfung XOR
>>> print(a ^ b) 
12

Schauen wir uns einmal die Zahlen 2 und 6 binär an:

          AND   OR    XOR
10        1010  1010  1010
 6        0110  0110  0110
Ergebnis  0010  1110  1100

Die Binärzahl 0110 kann man als 0 mal 8 + 1 mal 4 + 1 mal 2 + 0 mal 1 lesen.

Die AND-Verknüpfung ist nur dann erfüllt wenn an beiden Stellen eine Eins steht. Daher ist das Ergebnis 0010 oder 2 in dezimaler Schreibweise.

Bei der OR-Verknüpfung entsteht im Ergebnis eine Eins wenn an einer oder beiden Stellen eine Eins steht. Derher ergibt sich wieder 6 bzw. 0110.

Beim XOR ist die Bedingung nur erfüllt, wenn an einer der Stellen eine Eins und an der anderen eine Null steht. Stünden an beiden Stellen Einsen oder Nullen dann ergibt das wieder 0. Dadurch kommt das Ergebnis 4 (0100) zu Stande.

>>> print(b >> 1) 
3
>>> print(b << 1)
12

Der Schiebe-Operator verschiebt die Bits um eine Anzahl von Stellen (hier 1) nach Links oder Rechts.

               Ursprungswert    >> 1    << 1
Binär          0110             0011    1100
Dezimal        6                3       12

c) Logische Operatoren

Logische Operatoren werden in der Regel dazu verwendet, Vergleiche zu Verknüpfen und arbeiten mit Boolean-Werten.

Wert 1    True    True     False    False
Wert 2    True    False    True     False
and       True    Flase    False    False

Der and-Operator ist nur dann erfüllt, wenn beide Werte True sind. In jedem anderen Fall wird False geliefert.

Wert 1    True    True     False    False
Wert 2    True    False    True     False
or        True    True     True     False

Der or-Operator liefert True wenn einer oder beide Werte True sind und nur dann ein False wenn beide Werte False sind.

Wert 1    True     True    False    False
Wert 2    True     False   True     False
!=        False    True    True     False

Wenn Sie nun einen logischen xor-Operator vermissen, dann kommt hier wieder die Vereinheitlichung der Lösungswege von Python durch. Diesen logischen Operator gibt es nicht aber dafür kann der Ungleich-Operator != verwendet werden.

>>> print(5 != 6)
True
>>> print(bool(5) != bool(6)) 
False

Natürlich ist 5 nicht gleich 6 und daher ist die erste Ausgabe vollkommen logisch. Bei der Umwandlung in den Typ Boolean werden alle posiiven und negativen Werte zu True. Lediglich der Wert 0 wird zu False.

>>> a = 3 > 2
>>> a
True
>>> not a
False
>>> b = not 3 > 2
>>> b
False

Der Operator not negiert einen Boolean-Wert und liefert das Gegenteil.

d) Vergleichsoperatoren

Diese Operatoren dienen dazu Werte miteinander zu vergleichen. Dazu zählen die folgenden Operatoren:

<  ... kleiner
<= ... kleiner oder gleich
>  ... größer
>= ... größer oder gleich
!= ... ungleich
== ... gleich
is ... gleiche Instanz
in ... Element einer Liste / Teilstring

Die Operatoren lassen sich sowohl auf Strings

>>> print("aab" > "aad")
False
>>> print("aab" < "aad") 
True
>>> print("aab" >= "aad") 
False
>>> print("aab" <= "aad") 
True
>>> print("aab" == "aad") 
False
>>> print("aab" != "aad") 
True

als auch auf Zahlen anwenden

>>> print(5 > 4) 
True
>>> print(5 < 4) 
False
>>> print(5 >= 4) 
True
>>> print(5 <= 4) 
False
>>> print(5 == 4) 
False
>>> print(5 != 4) 
True

Bei Strings wird dem Zahlenwert der jeweils ersten Buchstaben verglichen, stimmt dieser überein, wird der selbe Vergleich mit den jeweils zweiten Buchstaben gemacht. Dies wird bis zu einem Unterschied durchgeführt oder bis die ganzen Zeichen durchlaufen sind.

>>> a = 5
>>> b = 2.5 * 2
>>> print(a == b)
True
>>> print(a is b)
False

Der Vergleichsoperator (==) arbeitet lediglich mit den Werten. Daher überrascht es nicht, dass 5 das gleiche ist wie 2 * 2.5.

Der is-Operator hingegen prüft ob die ID des Wertes im Speicher übereinstimmt. Dazu müssen wir uns kurz ansehen wie Python Werte im Speicher ablegt.

>>> a = 5
>>> b = 3 + 2
>>> print(id(a)) 
4297644544
>>> print(id(b)) 
4297644544
>>> print(a is b) 
True

Beide Variablen zeigen also auf den gleichen Wert im Speicher. Python versucht so effizient wie möglich zu arbeiten und da es nach der Ausführung von a = 5 den Wert 5 schon im Speicher gibt macht es wenig Sinn für eine weitere 5 im Speicher Platz zu reservieren und den Wert doppelt vorzuhalten. Es ist schneller in dem Fall beide Variablen auf die gleiche ID im Speicher zeigen zu lassen.

>>> b = 2.5 * 2
>>> print(id(b)) 
4322300120
>>> print(a is b) 
False

Ändert sich der Wert oder Datentyp von b nun dann wird die 5.0 im Speicher mit einer eigenen ID angelegt und b mit der neuen ID verknüpft.

Daher kann der is-Operator auch sicherstellen, dass eine Variable nicht nur den gleichen Wert hat, sondern auch vom gleichen Datentyp ist. Das muss ja auch so sein, wenn beide Variablen den gleichen Bereich im Speicher referenzieren.

Alternativ wäre auch
print(type(a) == type(b))

eine Prüfung des Datentyps alleine machbar.

e) Zuweisungsoperator

Ohne zu wissen wie dieser Operator heißt haben wir damit schon sehr oft gearbeitet... Das einfache = Zeichen wird verwendet um einer Variable einen Wert zuzuweisen.

>>> var1 = "Wert"
>>> var2 = 123
>>> print(var2)
123

>>> var2 = var1 
>>> print(var2) 
Wert
>>> print(id(var1)) 
4324866008
>>> print(id(var2)) 
4324866008

Hierbei können wir einen Wert direkt zuweisen (var2 = 123) oder den Wert einer Variable zuweisen (var2 = var1).

>>> var2 = "abc" 
>>> print(var1 + " " + var2) 
Wert abc
>>> print(id(var1)) 
4324866008
>>> print(id(var2)) 
4320943832

Wenn wir einer Variable den Wert einer anderen zuweisen (var2 = var1) dann sind diese Variablen danach immer noch unabhängig von einander... Wenn in weiterer Folge var2 wieder geändert wird (var2 = "abc") bleibt var1 davon unberührt.

Anfänger werden oft davon verwirrt, dass nach einer Zuweisung wie var2 = var1 beide Variablen auf die gleiche ID zeigen. Dies ist jedoch nur temporär solange bis sich der Wert von var2 ändert. Sobald das geschieht wird ein neuer Speicherbereich mit einer neuen ID erstellt und var2 zugewiesen.

var1 bleibt wie man sieht davon unberührt.

Typenkonvertierung

In vielen Fällen, wenn es eindeutig klar ist was der Entwickler will, nimmt uns Python die Typenkonvertierung ab. Dies ist im Vergleich zu manch anderer Programmiersprache recht komfortabel.

>>> a = 10
>>> b = 10
>>> print(str(b) + " == " + str(type(b)))
10 == <class 'int'>
>>> b = a * 2.1
>>> print(str(b) + " == " + str(type(b)))
21.0 == <class 'float'>
>>> b = a * 3.55
>>> print(str(b) + " == " + str(type(b))) 
35.5 == <class 'float'>
>>> b = "3.55"
>>> print(b + " == " + str(type(b))) 
3.55 == <class 'str'>

Beim überschreiben der Variable von Hand wie in den letzten drei Zuweisungen wird der Typ automatisch konvertiert. Auch hier haben wir einen Vorteil gegenüber manch anderen Sprachen wo man den Variablentyp schon beim ersten Anlegen vordefinieren muss und alle Konvertierungen von Hand vornehmen muss.

Bei anderen Fällen ist aber nicht eindeutig klar, was Python machen soll...

>>> s = "b"
>>> print(s + 3)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: Can't convert 'int' object to str implicitly

Der TypeError sagt uns, dass wir einen String brauchen. In diesem Fall ist die Meldung wenig hilfreich weil diese den Fehler nicht wirklich beschreibt. Besser wäre eine Meldung die sagt, dass eine eindeutige Typenkonvertierung von Hand nötig ist.

Wir versuchen an dieser Stelle b und 3 mit dem + Operator zu verbinden. Wenn wir uns zurückerinnern dann kann der + Operator Zahlen addieren und Strings verbinden. An dieser Stelle liefern wir dem Interpreter aber eine Zahl und einen String also weiß er nicht ob er nun b3 liefern soll, b als Hexadezimalwert für 11 nehmen soll und damit 14 liefern oder sollte er gar den ASCII-Wert von b - die Zahl 98 - mit 3 addieren und dann 101 als Ergebnis bzw. e liefern.

Daher sagt uns Python mit einem TypeError "Lieber Programmierer deine Anweisung ist mehrdeutig - kümmere dich selber um die Typenkonvertierung".

Wenn wir print(s + str(3)) schreiben ist es eindeutig und wir erhalten b3 als Ausgabe.

Die gängisten Möglichkeiten sind:

int(var) ..... Konvertiert var in eine Ganzzahl
float(var) ... Konvertiert var in eine Fließkommazahl
str(var) ..... Konvertiert Objekt var in eine String-Darstellung
repr(var) .... Konvertiert Objekt var in einen String-Ausdruck
eval(var) .... Wertet String var aus und gibt ein Objekt zurück
tuple(var) ... Konvertiert Sequenz var in ein Tupel
list(var) .... Konvertiert Sequenz var in eine Liste
chr(var) ..... Konvertiert die Ganzzahl var in ein Zeichen
ord(var) ..... Konvertiert das Zeichen in var in dessen Zahlwert

Also hier nochmals die zuvor genannten 3 bzw. 4 Varianten:

>>> s = "b"
>>> print(s + str(3)) 
b3
>>> print(int(s, 16) + 3)
14
>>> print(ord(s) + 3) 
101
>>> print(chr(ord(s) + 3)) 
e

Durch die Angabe von 16 innerhalb der int() Funktion als zweiten Parameter wird Python mitgeteilt, dass mit der Basis 16 (hexadezimal) gearbeitet wird. Ein print(int("110", 2) + 3) würde dann 9 liefern weil 110 in binärer Schreibweise die Zahl 6 ergibt wie wir schon bei den bitweisen Operatoren festgestellt hatten.

Bei der Umwandlung von Kommazahlen zu Ganzzahlen werden die Kommastellen direkt verworfen. Dies entspricht einem Abrunden.

>>> f = 3.8
>>> print(str(int(f))) 
3
>>> print(str(int(f) + int(f))) 
6
>>> print(str(int(f + f))) 
7

Wir müssen also in solchen Fällen unbedingt darauf achten wann wir eine Konvertierung durchführen. Vergleichen Sie dazu die Ergebnisse von Anweisung Nr. 3 und 4.

Wiederholungen & Verzweigungen

In quasi jedem Programm kommt es vor, dass auf bestimmte Umstände reagiert werden muss, sei es eine Benutzereingabe oder das Auftreten eines bestimmten Wertes in einer Datei die verarbeitet wird.

Darüber hinaus ist es auch sehr oft der Fall, dass bestimmte Schritte eines Programmes mehrfach wiederholt werden müssen.

#!/usr/bin/python3
for i in range(1,5):
    print(str(i)+": ", end="")
    if i == 2:
        print("i ist 2")
    elif i > 2:
        print("i ist größer als 2")
    else:
        print("nichts anderes trifft zu")

Schreiben Sie die oben aufgeführten Zeilen in einen Text-Editor Ihrer Wahl. Sie können dazu zB IDLE verwenden indem Sie eine neue Datei erstellen oder wenn Sie Komfortabel arbeiten wollen dann würde ich Ihnen Microsofts Visual Studio Code empfehlen. Ich habe die Datei dann als for_if.py abgespeichert und kann das Script dann wie folgt aufrufen:

user@kali:~/PY_BUCH/000_Scripts/$ python3 for_if.py
1: nichts anderes trifft zu
2: i ist 2
3: i ist größer als 2
4: i ist größer als 2

Die ersten beiden Zeilen liefern die Zahlen 1 bis 4 am Anfang der Ausgabe. Die for-Schleife bietet sich an, wenn wir eine bestimmte Anzahl von Wiederholungen durchführen wollen. Schleifen wiederholen einen Block von Anweisungen entweder für eine vordefinierte Anzahl an Durchgängen oder bis ein bestimmter Fall (zB Ende der Datei, alle Listenelemente durchalufen, usw.) eintritt.

Danach folgen die verschiedenen Verzweigungen. Die einfachste Variante wäre ein if-Block ohne elif und ohne else. Wenn die Bedingung vom if-Block zutrifft wird der eingerückte Anweisungsblock ausgeführt.

Falls nicht, werden der Reihe nach die Bedingungen des ersten, zweiten, dritten, n-ten elif-Blockes geprüft. Tifft eine der Bedingungen zu, dann wird dieser Block ausgeführt. Wenn nicht, dann geht die Prüfung mit dem nächsten elif-Block weiter. Trifft keine einzige Bedingung zu dann werden die Anweisungen des else-Block ausgeführt (falls dieser vorhanden ist).

Außerdem ist for die Schleife der Wahl um alle Elemente von Listen oder Dictionaries zu durchlaufen. Zuerst erstelle ich die Datei for_list.py mit folgendem Inhalt:

#!/usr/bin/python3
l = ["DDD", "EEE", "FFF"]
for entry in l:
    print(entry + " ", end="")
    print("")

Dann können wir das Script wieder ausführen:

user@kali:~/PY_BUCH/000_Scripts/$ python3 for_list.py
DDD EEE FFF

Mit Hilfe von for entry in d.keys() bekommen wir eine Liste der Schlüssel eines Dictionaries und dann könnten wir mit dem Schlüssel auf den Wert des Dictionary-Eintrags zugreifen. Alternativ können wir auch for entry in d.values() verwenden um nur die Werte ohne die Schlüssel zu bekommen.

Ein Dictionary biete außerdem die items() Methode um komfortabel auf ein Key : Value Paar zuzugreifen. Diese Methode liefert eine Liste von Tupeln, die wir dann gleich im Schleifenkopf entpacken (auflösen zu einzelnen Variablen - hierbei muss die Anzahl der Tupel-Elemente mit der Anzahl der Variablen übereinstimmen).

#!/usr/bin/python3
d = {"a" : "AAA", "b" : "BBB", "c" : "CCC"}
for key, value in d.items():
    print(key + " => " + value)

Bei der Ausführung erhalten wir dann:

user@kali:~/PY_BUCH/000_Scripts/$ python3 for_dict.py
a => AAA
b => BBB
c => CCC

Es gibt aber auch Fälle in denen wir die Anzahl der Wiederholungen nicht kennen. Bespielsweise wenn User Daten eingeben oder Dateien durchlaufen werden ist die Anzahl der Eingaben oder Zeilen in der Regel nicht bekannt. Für solche Fälle gibt es die while-Schleife.

#!/usr/bin/python3
import random
random.seed()
rand_number = random.randint(0, 9)
right_guess = False

while not right_guess:
    guess = int(input("Zahl zwischen 0 und 9 eingeben: "))
    if guess == rand_number:
        right_guess = True
    elif guess > rand_number:
        print("Die gesuchte Zahl ist kleiner")
    else:
        print("Die gesuchte Zahl ist größer")

print("GEWONNEN! Die Zahl " + str(rand_number) + " wurde gesucht!")

Mit der import-Anweisung laden wir zusätzliche Module in unser Programm. So ein Modul stellt dann Klassen und Funktionen zur Verfügung. Was genau das ist, erfahren wir später. Hier wird mit import random die Funktionalität zum Erzeugen von Zufallszahlen geladen.

Das random.seed() sorgt dafür, dass der Zufallszahlengenerator bei jedem Aufruf einen anderen Startwert bekommt. Damit ergibt sich auch immer eine andere Reihenfolge der Zahlen. Danach wird mit random.randint(0, 9) eine Zufallszahl zwischen 0 und 9 generiert.

Mit input(...) wird dann der Text ausgegeben und eine Benutzereingabe als String eingelesen welcher dann gleich in einen int-Wert konvertiert und der Variable guess zugewiesen wird.

Bei einer while-Schleife ist zu bedenken, dass wir als Entwickler dafür verantwortlich sind die Abburchbedingung irgendwann zu erreichen. Würde man die Zeile rightGuess = True entfernen, dann würde das Programm endlos laufen ohne sich je zu beenden.

Dann spielen wir eine Runde und testen unser Programm:

user@kali:~/PY_BUCH/000_Scripts/$ python3 while_loop.py
Zahl zwischen 0 und 9 eingeben: 5
Die gesuchte Zahl ist größer
Zahl zwischen 0 und 9 eingeben: 8
Die gesuchte Zahl ist kleiner
Zahl zwischen 0 und 9 eingeben: 7
GEWONNEN! Die Zahl 7 wurde gesucht!

Kommentare

Ein umfangreicheres Programm oder Script kann schnell einige hundert bis zu hunderttausenden Zeilen haben. Daher ist es wichtig sich selbst oder auch anderen Programmierern Hinweise im Quellcode zu hinterlassen. Oftmals wird dies auch am Beginn der Datei gemacht indem ein mehrzeiliger String dazu zweckentfremdet wird. Kommentare beginnen mit dem #-Zeichen und alles dahinter ignoriert der Interpreter. Sehen wir uns als Beispiel folgendes Programm an:

#!/usr/local/bin/python3
"""
Mein supertolles Programm zum errechnen des Quadrates einer Zahl
Systemvoraussetzung: Python Version 3.x
Lizenz:              GPLv3
(c) Ich Selber 2018 
"""

# Einlesen der Zahl
number = input("Zahl eingben: ")

# Komma in Punkt umwandeln
number = str(number).replace(",", ".")

# Prüfen ob die Eingabe eine Zahl ist
if number.isnumeric():
    # Errechnen des Erg. und Ausgabe
    res = float(number) ** 2
    print("Das Quadrat von " + str(number), end="") # end = "" nur
    print(" ist: " + str(res))                      # damit es unter Python 2.x nicht läuft :P
else:
    # Fehler ausgeben
    print("Ihre Eingabe ist keine gültige Zahl!")

In der ersten Zeile wird das Pseudo-Kommentar für Unix- und Linux-Systeme angegeben. Dort kann man eine einfache Textdatei als ausführbar markieren und dann wird anhand dieses Pseudokommentares der Interpreter bestimmt. Danach kommt unser zweckentfremdeter mehrzeiliger String in dem diverse Infos zum Programm, Systemvoraussetzungen, Lizenz, Danksagungen, beteiligte Entwickler uvm. untergebracht werden können. Danach folgen einige sogenannte einzeilige Kommentare, die mit dem #-Zeichen eingeleitet werden. Diese Kommentare können sowohl über einer Programmzeile als auch am rechten Ende der Zeile stehen.

Funktionen

Wir haben bisher schon des Öfteren die Begriffe Methoden und Funktionen gehört. Daher werden wir zunächst einmal klären was eine Funktion überhaupt ist.

Unter einer Funktion verstehen wir einen in sich abgeschlossenen Code-Block. Dies wird verwendet um wiederkehrende Aufgaben vom Hauptprogramm abzuspalten und zentral zu verwalten. Außerdem wird der Code einfacher zu warten da so wiederkehrende Blöcke zentral in einer Funktion stehen und damit an einer zentralen Stelle veränderbar sind.

Sehen wir uns einfach ein Beispiel an:

#!/usr/local/bin/python3
PI = 3.14
def circle_area(r):
    area = r ** 2 * PI
    return area

print(circle_area(2))
print(circle_area(4))
print(circle_area(8))
print(area)

Dann lassen wir das Script laufen und sehen uns an was wir erhalten:

user@kali:~/PY_BUCH/000_Scripts/$ python3 circle_area_func.py
12.56
50.24
200.96
Traceback (most recent call last):
  File "/Users/mac/Buch_Hacken_mit_Python/00_Scripts/00_while.py", line 12,
in 
    print(area)
NameError: name 'area' is not defined

Hier sehen wir beide Methoden um Daten an eine Funktion zu übergeben - den Funktionsparameter (r) der beim Aufruf der Funktion übergeben wird und die Variable PI, die außerhalb der Funktion definiert ist. Hierbei gilt das die Funktion auf Variablen von draußen zugreifen kann der Zugriff von Außen auf eine Funktionsvariable ist jedoch nicht erlaubt.

Darum gibt es auch das return-Schlüsselwort mit dem definiert wird welcher Wert von der Funktion wieder zurückgegeben wird. So bekommen wir Werte wieder aus der Funktion heraus. Hierbei können wir aber nur eine einzige Variable zurückgeben.

Sollen mehrere Werte zurückgeliefert werden müssen wir eine Liste, ein Dictionary oder einen Tupel mit den benötigten Werten zurückgeben. Ein Beispiel hierfür wäre die items() Methode von Dictionaries, die zB eine Liste von Tupeln liefert.

Dies wäre aber keine Regel gäbe es nicht auch eine Ausnahme:

Übergeben wir einer Funktion eine Liste, ein Dictionary oder einen Tuple dann ändert sich dieses Verhalten! In so einem Fall wird keine Kopie des Wertes übergeben sondern eine sogenannte Referenz auf das Objekt. Das kann man sich wie eine Verknüpfung unter Windows oder einen Link unter Unix/Linux vorstellen.

Die Referenz ist einfach ein Zeiger auf den Bereich im RAM-Speicher in dem die Original-Daten liegen. Hier würden also die Originaldaten verändert.

Man kann Python allerdings zwingen die Daten zu kopieren indem man statt einer Liste ein Slice mit allen Werten (liste[:]) übergibt. Je nach Menge der Einträge kann die Performance des Programms darunter spürbar leiden.

Arbeiten mit Dateien

Bisher habe wir nur sehr einfache Programme geschrieben aber selbst bei dem Ratespielchen wäre es schon sinnvoll gewesen den Highscore in einer Liste abzuspeichern.

Die Variablen, die ihre Daten im RAM-Speicher ablegen sind flüchtig. Wird das Programm beendet wird dieser Speicher wieder freigegeben und die Daten sind verloren. Wollen wir Daten dauerhaft ablegen dann müssen wir diese auf die Festplatte oder in eine Datenbank schreiben.

with open("daten.txt", "w") as file:
    file.write("bla")
    file.write("blub")
    file.write("foo")

Hierbei sorgt das with open("daten.txt", "w") as file dafür, dass die Datei nach dem Beenden des Blockes auch wieder sauber geschlossen wird. Das hat den Vorteil, dass wir das nicht vergessen können.

Alternativ können wir auch file = open("daten.txt", "w") schreiben. Danach müsste man die write-Befehle nicht einrücken aber dafür die Datei mit file.close() wieder schließen wenn wir fertig sind.

Der open-Befehl benötigt zwei Parameter - den Dateinamen inkl. Pfad bzw. falls wir keinen Pfad angeben sucht Python automatisch im gleichen Ordner wie das Script und den Modus. Für den Modus gibt es folgende Optionen:

a .... Anfügen von Daten am Ende der Datei (append)
r .... Lesen (read)
w .... Schreiben bzw. Überschreiben (write)
ab ... Anfügen im binären Modus (append binary)
rb ... Lesen im binären Modus (read binary)
wb ... Schreiben bzw. Überschreiben im binären Modus (write binary)

Wird eine bereits vorhandene Datei zum Schreiben geöffnet wird der alte Inhalt vollständig gelöscht und ersetzt. Selbst wenn der neue Inhalt kürzer ist gehen alle vorherigen Zeilen des Inhaltes verloren. Dies geschieht auch ohne jegliche Sicherheitsfrage wenn wir diese nicht selbst programmieren. Daher sollte man ein solches Programm ausführlich mit Dummy-Dateien testen bevor man es auf wichtige Systemdateien oder ähnliches loslässt.

Wenn wir die Datei in einem Editor öffnen erhalten wir folgenden Inhalt:

blablubfoo

So war das eigentlich nicht geplant. Wir müssen also beim Schreiben der Datei zusätzlich einen Zeilenumbruch an die Daten anfügen, wenn wir mehrere Zeilen schreiben wollen. Das erreichen wir beispielsweise mit file.write("bla\n").

Wenn wir Überprüfen wollen, ob eine Datei oder ein Verzeichnis existiert bzw. ob wir schreibend auf eine Datei zugreifen dürfen, dann haben wir folgende Optionen:

import os
>>> print(os.path.exists("/bin"))
True
>>> print(os.path.isdir("/bin")) 
True
>>> print(os.path.isfile("/bin"))
False
>>> print(os.path.exists("/bin/sh")) 
True
>>> print(os.path.isdir("/bin/sh")) 
False
>>> print(os.path.isfile("/bin/sh")) 
True
>>> print(os.access("/bin/sh", os.W_OK)) 
False
>>> print(os.access("/bin/sh", os.R_OK)) 
True
>>> print(os.access("/bin/sh", os.X_OK)) 
True

Die Methode exists() überprüft nicht, ob es sich um eine Datei oder einen Ordner handelt sondern lediglich ob der Pfad existiert. Mit isfile() und isdir() lässt sich feststellen ob es sich um eine Datei oder einen Ordner handelt.

os.access() überprüft ob auf eine Datei oder einen Ordner in einem bestimmten Modus zugegriffen werden kann. Der erste Parameter ist der Pfad und der zweite Parameter der Modus. Hierbei prüft os.W_OK ob Schreibzugriff erlaubt ist, os.R_OK ob Lesezugriff erlaubt ist und os.X_OK ob die Datei ausführbar ist.

Achtung!!!

Unter Linux und Unix müssen Ordner ebenfalls ausführbar sein um geöffnet zu werden.

Danach können wir die Datei mit folgendem Code wieder einlesen:

with open("daten.txt", "r") as file:
    for line in file:
        print(line.rstrip())

Jetzt erhalten wir als Ausgabe:

bla
blub
foo

Da die print() Funktion selbst einen Zeilenumbruch anfügt und beim lesen der Zeile der Zeilenumbruch ebenfalls als Teil der Zeile mitgeliefert wird benötigen wir ein rstrip() um die sogennannten Whitespaces (Leerzeichen, Tabulatoren, Zeilenschaltungen, etc.) am rechten Ende der Zeile zu entfernen. Andernfalls wäre bei der Ausgabe zwischen den Zeilen immer eine Leerzeile.

Für die Arbeit mit XML-Dateien kann ich Ihnen das Modul xmltodict empfehlen. Wie der Name vermuten lässt sorgt dieses Modul dafür, dass aus den XML-Daten ein Python-Dictionary wird mit dem Sie dann wie bereits erklärt arbeiten können.

Objektorientierte Programmierung

Objektorientierte Programmierung (OOP) ermöglicht es zusammengehörige Logik in einer sogenannten Klasse zu bündeln. Außerdem kann diese Klasse private und öffentliche Eigenschaften (Daten) und Methoden (Funktionen) besitzen.

Damit ist es nicht nur möglich logische Einheiten zu bilden und so für mehr Struktur und Ordnung zu sorgen, sondern auch festzulegen welche Methoden und Eigenschaften dem Nutzer der Klasse zugänglich sein sollen und welche nur intern verwendet werden.

Vererbung ist ein weiteres wichtiges Konzept von OOP. Im Grunde ist es recht einfach - stammt eine Klasse von einer anderen Klasse ab dann werden die Eigenschaften und Methoden der Eltern-Klasse übernommen.

In der neuen Klasse können die übernommenen Eigenschaften und Methoden dann erweitert und/oder komplett überschreiben werden. Damit das Ganze etwas klarer wird sehen wir uns ein kleines Beispiel an...

#!/usr/local/bin/python3
class Vehicle():
    def __init__(self, model, manufacturer, price):
        self.__model = model
        self.__manufacturer = manufacturer
        self.__price = price

    def showInfo(self):
        return self.__manufacturer + " " + self.__model

    def getPrice(self):
        return self.__price

Soweit sollte alles klar sein. Hier legen wir eine Basis-Klasse an die sehr allgemein gehalten ist und ein Fahrzeug mit den Eigenschaften Modellname, Hersteller und Preis beschreibt. Die Methoden showInfo() und getPrice() liefern die Daten zurück.

Um den Code knapper und übersichtlicher zu halten habe ich keine Methoden zum Ändern der Daten implementiert.

class MotorBike(Vehicle):
    def __init__(self, model, manufacturer, price, hp, year, km):
        super().__init__(model, manufacturer, price)
        self.__hp = hp
        self.__year = year
        self.__km = km

    def showInfo(self):
        return super().showInfo() + ", " + str(self.__hp) + \
        " PS, EZ " + str(self.__year) + ", " + str(self.__km) + " km"

MotorBike, die zweite Klasse unseres Beispiels stammt von der Klasse Vehicle ab. Das wird durch die Schreibweise class KlassenName(ElternKlasse) erreicht. Das Klassennamen mit Großbuchstaben anfangen hat sich so eingebürgert und dient der Abgrenzung von den Variablennamen, die nach dieser Konvention mit einem Kleinbuchstaben beginnen.

In der __init__() Methode wird zuerst super().__init__(model, manufacturer, price) aufgerufen. Damit wird der Konstruktor der Elternklasse ausgeführt welcher die von dort geerbten Variablen model, manufacturer und price belegt. Da diese Variablen vererbt wurden müssen Sie nicht von Hand in der abgeleiteten Klasse angelegt werden. Genauso erlaubt der Aufruf von super().__init__(...) es den Konstruktor der Elternklasse in der aktuellen Klasse auszuführen und damit die drei Eigenschaften in dieser Klasse zu belegen.

Mit self.__hp = hp und den zwei folgenden Zeilen werden PS, EZ und gefahrene km als weitere Eigenschaften der Klasse definiert.

Bei der Ausgabe der Daten verfahren wir ähnlich. Zuerst holen wir die Ausgabe der Elternklasse mit super().showInfo() und ergänzen diese mit + ", " + str(self.__hp) + ... um die zusätzlichen Felder, die nur in dieser Klasse existieren.

class Car(MotorBike):
    def __init__(self, model, manufacturer, price, hp, year, km, doors):
        super().__init__(model, manufacturer, price, hp, year, km)
        self.__doors = doors

    def showInfo(self):
        return super().showInfo() + ", " + str(self.__doors) + "-türig"

Die Definition der Car-Klasse ist noch kürzer. Hier leiten wir die Klasse von der MotorBike-Klasse ab da diese so gut wie alle benötigten Eigenschaften dort bereits implementiert sind.

Das Vorgehen ist hierbei absolut das gleiche - wir rufen mit super()... die Funktionaliät aus der Elternklasse MotorBike auf und erweitern diese um die Eigenschaft __doors für die Türanzahl.

class Quad(MotorBike):
    pass

Die einfachste Klassendefinition ist unsere Quad-Klasse. Hier erstellen wir nichts weiter als eine eins zu eins Kopie der Klasse MotorBike unter einem neuen Namen. Die Anweisung pass wird benötigt da wir in Python keine End-Markierungen für Blöcke haben. Somit sagt das pass in dem Fall nur, dass der leere Block so gewollt ist.

Damit ist das Anlegen der Klassen fertig und wir können nun folgende Objekte erstellen:

v = Vehicle("City Fun 28.3", "KTM", 429.00)
b = MotorBike("CBR 125R", "Honda", 6990.00, 14, "05/2013", 6788)
c = Car("Fabia 1.9 TDI Kombi", "Skoda", 12990.00, 101, "09/2014", 37855, 5)
q = Quad("King 750 AXI", "Suzuki", 4990.00, 38, "04/2014", 7985)

Im Grunde ist das nichts anderes als 4 Variablen anzulegen und mit den jeweils geforderten werten zu füttern.

for obj in [v, b, c, q]:
    print(obj.showInfo(), end="")
    print(", VKP %.2f EUR" % obj.getPrice())

Danach durchlaufen wir die 4 Variablen mit einer for-Schleife. Hier wird Ihnen vielleicht auffallen, dass die Klassen Quad und Car beide von MotorBike abstammen und dass selbst in MotorBike keine Methode getPrice() angelegt wurde.

Diese Methode erbt MotorBike selbst von Vehicle und vererbt die Methode weiter an seine Kind-Klassen. Wenn wir so eine Klassenhirarchie aufbauen dann können wir den geschriebenen Code massiv verkürzen und die Wartungsfreundlichkeit erhöhen.

Natürlich macht es keinen Sinn mit Klassen und abgeleiteten Klassen zu arbeiten um ein kleines Wartungsscript mit wenigen Zeilen zu realisieren. Wenn wir aber an größeren Projekten arbeiten ist OOP der beste Weg um kompakten Code zu schreiben den wir auch gut für andere Projekte wiederverwenden können.

Lassen wir unser Programm laufen erhalten wir folgendes:

KTM City Fun 28.3, VKP 429.00 EUR
Honda CBR 125R, 14 PS, EZ 05/2013, 6788 km, VKP 6990.00 EUR
Skoda Fabia 1.9 TDI Kombi, 101 PS, EZ 09/2014, 37855 km, 5-türig, VKP 12990.00 EUR
Suzuki King 750 AXI, 38 PS, EZ 04/2014, 7985 km, VKP 4990.00 EUR

Wir erhalten für jedes Objekt die passende Ausgabe. Genau das was wir wollten - im Grunde können wir vier verschiedene Objekte mit der gleichen Methode behandeln und erhalten dennoch immer die passende Ausgabe mit den kleinen Unterschieden.

Wenn wir genau nachvollziehen wollen wie sich so ein Objekt zusammensetzt dann können wir den Aufbau mit

print(c.__dict__)

ausgeben lassen. Bei unserem Auto erhalten wir beispielsweise:

{
    '_Vehicle__model': 'Fabia 1.9 TDI Kombi',
    '_Vehicle__manufacturer': 'Skoda',
    '_Vehicle__price': 12990.0,
    '_MotorBike__hp': 101,
    '_MotorBike__year': '09/2014',
    '_MotorBike__km': 37855,
    '_Car__doors': 5
}

Fehlerbehandlung

Selbst wenn wir versuchen mögliche Fehler zu bedenken - in vielen anderen Fällen steigt die Anzahl möglicher Fehler extrem an und wir werden schwerlich jeden dieser Fehler im Vorfeld bedenken können.

Nehmen wir dazu einfach das Laden von Daten aus dem Internet: Der Server kann nicht erreichbar sein, die Verbindung kann während der Übertragung abreißen, die Datei kann gelöscht worden sein, der Zugriff auf die Datei wird verweigert wegen unzureichender Rechte, der Download kann erfolgen aber die Datei beim Transport beschädigt werden, die Datei kann überschrieben worden sein und nun einen ganz anderen Inhalt haben als unser Programm erwartet, uvm.

Sie sehen wir haben nicht nur eine umfangreiche Liste an möglichen Fehlern, sondern einige der Fehler kann man vorab gar nicht ausschließen. So kann man vorab unmöglich ausschließen, dass eine Datei beim Download beschädigt wird und nach dem Download zu testen, ob die Datei intakt ist gestaltet sich oft äußerst schwierig wenn man keine MD5 oder SHA Checksumme hat.

Daher ist es in vielen Fällen effizienter Fehler abzufangen und darauf zu reagieren. Vor allem stellt es sicher, dass wir keinen möglichen Fehler vergessen haben. Und so können wir das in Python realisieren:

#!/usr/local/bin/python3
try:
    print("Try-Block Beginn")
    with open("xxx", "r") as file:
        print(file)
        for line in lines:
            print(30 / int(line.strip()))
        print("Weitere Anweisungen im Try-Block")

    except FileNotFoundError:
        print ("Kann xxx nicht finden")

    except:
        print ("Unerwarteter Fehler aufgetreten")

print("Weitere Anweisungen nach dem Try/Except-Block")

Sehen wir uns einmal die Ausgabe des Scriptes an:

Try-Block Beginn
Kann xxx nicht finden
Weitere Anweisungen nach dem Try/Except-Block

Wir sehen den Beginn des try-Blocks, da die Datei xxx nicht existiert tritt ein Fehler im with-Block auf und der ganze try-Block wird abgebrochen. So wird auch der print-Befehl "Weitere Anweisungen im Try-Block" nach dem with-block übersprungen.

Daher bietet es sich an im try-Block allen Code unterzubringen der davon abhängt, dass das Lesen der Datei klappt.

Tritt ein Fehler auf wird der passende except-Block ausgeführt. In unserem Fall ist hier ein FileNotFoundError aufgetreten und daher wird auch dieser Block abgearbeitet. Um zu zeigen was passiert wenn es klappt, legen wir die Datei xxx mit einer Zeile in der 10 steht an.

user@kali:~/PY_BUCH/000_Scripts/$ echo "10" > xxx

Danach können wir das Script wieder laufen lassen:

Try-Block beginn
<_io.TextIOWrapper name='xxx' mode='r' encoding='UTF-8'>
3.0
Weitere Anweisungen im Try-Block
Weitere Anweisungen nach dem Try/Except-Block

Alle Programmanweisungen wurden wie gewünscht durchlaufen. Zuerst wird der Dateihandler ausgegeben und danach wir das Resultat der Berechnung 30 / 10 ausgegeben.

Um zu sehen wozu wir den zweiten except-Block brauche werden wir einen weiteren Fehler erzeugen. Dazu werden wir eine weitere Zeile hinzufügen, die die Zahl 0 enthält. So simulieren wir, dass in einer Datei unerwartete Einträge enthalten sind. Dadurch wird ein anderer Fehler (ZeroDivisionError) auftreten.

user@kali:~/PY_BUCH/000_Scripts/$ echo "0" >> xxx

Und wir lassen das Script wieder laufen:

Try-Block beginn
<_io.TextIOWrapper name='xxx' mode='r' encoding='UTF-8'>
3.0
Unerwarteter Fehler aufgetreten
Weitere Anweisungen nach dem Try/Except-Block

Die Zeile Weitere Anweisungen im try-Block wird übersprungen, da bei der Berechnung von 30 / 0 ein Fehler auftritt. Da wir nicht explizit einen except-Block für den ZeroDivisionError angelegt haben, wird der except-Block ohne spezifizierten Fehler ausgeführt.

Natürlich ist der Hinweiß auf einen nicht erwarteten Fehler wenig hilfreich für den User aber immer noch besser als ein Programmabsturz mit Datenverlust. Es wäre genausogut denkbar gewesen , dass Sie keine Berechtigung haben auf die Datei zuzugreifen. Dies kann dadurch geschehen, dass bereits jemand anders mit der Datei arbeitet oder ein Administrator Ihnen den Zugriff auf diese Datei entzogen hat.