3. Übung

Beispiel 3.1

Im Rahmen dieses Beispiels wollen wir im Wesentlichen die Liste als neuen Datentypen sowie die for-Schleife als neues Programmierkonstrukt kennenlernen.

Zeichenketten

In den ersten beiden Übungen haben wir schon einige der elementaren Datentypen, die Python - wie jede andere Programmiersprache auch - besitzt. Darunter waren mit integer und float zwei Datentypen, mit denen wir numerische Werte repräsentieren können. Wenn wir

a = 42
f = 3.1415

codieren, so speichern wir EINEN numerischen Wert vom Typ integer unter dem Namen a ab und EINEN numerischen Wert vom Typ float unter dem Namen f ab. Wenn wir hingegen

s = 'Ich bin ein String'

in unser Programm schreiben, dann legen wir EINE ZeichenKETTE unter dem Namen s an, die aber aus mehreren einzelnen Zeichen aufgebaut sind, die jedes für sich auch einen eigenen Wert darstellen, der in einer getrennten Variablen gespeichert sein könnte:

l11 = 'I'
l12 = 'c'
l13 = 'h'
s3 = 'auch'
space = ' '

s = l11 + l12 + l13 + space + 'bin' + space + s3 + ' '+ 'ein String'

Wir können Strings aber nicht nur aus einzelnen Zeichen und aus anderen Strings zusammensetzen, sondern wir können umgekehrt Strings auch wieder in einzelne Buchstaben oder in Teilstrings zerlegen. Und, wir können auch auf die einzelnen Zeichen eines Strings zugreifen.

s = 'Ich bin ein sieben Worte langer String'

"""
In String fängt die Positionsindizierung bei 0 an. Die Indizes eines String mit der Länge n
sind also 0...n-1.
(Und dies ist ein mehrzeiliger Kommentar, den man in dreifache " setzt.)
"""
print(s[0])       # 1. Zeichen des Strings s an Position 0
print(s[1])       # 2. Zeichen des Strings s an Position 1
print(s[2])       # 3. Zeichen des Strings s an Position 2
print(s[4:7])     # Zeichen des Strings s an den Positionen 4...6
print(s[-1])      # 1. Zeichen des Strings vom Ende her, also an Position n-1
print(s[0:10:2])  # jedes 2. Zeichen von s zwischen den Positionen 0...9
print(s[::2])     # jedes 2. Zeichen von s von 0...n-1
print(s[::-1])    # jedes Zeichen von s von n-1...0

"""
Die allgemeine Syntax lautet also:
[Anfang : Ende+1 : Schrittweite und Schrittrichtung]
Defaultwerte:
[0 : n : +1]  (n = Länge des Strings)
"""

Merke

Da Zeichen in der Form von ZeichenKETTEN in der Datenverarbeitung (neben Zahlenwerten) eine besondere Bedeutung haben, gibt es eine Vielzahl besonderer Operationen und Funktionen, die wir auf diese anwenden können. (Das Modul string enthält viele dieser Funktionen, von denen wir später noch einige kennenlernen werden.)

Listen

Listen sind ein sog. zusammengesetzter Datentyp, der eine gewisse Ähnlichkeit zu den o.a. Strings hat. Unter der Haube gibt es dann aber doch eine ganze Reihe Unterschiede. In einer Liste können wir zunächst - ähnlich den Zeichenketten - mehrere Datenwerte abspeichern, auf die wir dann mit dem Indexoperator [ ] zugreifen können:

eine_liste = [42, 9, 17+4, 99]

print(eine_liste[0])                # 1. Element der Liste
print(eine_liste[2])                # 3. Element der Liste
print(eine_liste[-1])               # 1. Element der Liste von hinten
print(eine_liste[2:4])              # 3.-4. Element
print(eine_liste[::-1])             # alle Elemente in umgekehrter Reihenfolge
print(eine_liste[0]+eine_liste[1])  # Rechnen können wir damit natürlich auch

# die Anzahl der Elemente einer Liste bestimmen wir mit der Funktion *len()*
print('Länge der Liste: ', len(eine_liste))

# wir schauen uns die Datentypen der Listenelemente an und ...
i_n = 0
while i_n < len(eine_liste):
  print(type(eine_liste[i_n]))
  i_n += 1

# ... die Liste selber hat auch einen Datentyp
print('\n', type(eine_liste))

Merke

Die Methode aus einer größeren Datenmenge wie einer Liste nur eine Teilmenge herauszunehmen in der Form liste[2:8:3], in diesem Fall also jedes dritte Element im Indexbereich 2..7, nennt sich offiziell Slicing. (Dieses Verfahren findet auch in MATLAB Verwendung.)

Natürlich können wir auch Listen mit anderen Datentypen anlegen:

f = 4.4
b = True
liste_f = [1.1, 2.2, 3.3, f]
liste_b = [True, False, False, b]

print(liste_f[0])                # erstes Element von liste_f
print(liste_b[-1])               # letztes Element von liste_b

Wir können aber auch Listen anlegen, in denen die Elemente alle unterschiedliche Datentypen besitzen:

q_liste = [42, 3.1415, True, 'Python', [1, 2, 3]]

# wir schauen uns die Datentypen und -werte der Listenelemente an
i_n = 0
while i_n < len(q_liste):
  print('Datentyp: ', type(q_liste[i_n]), ' Datenwert= ', q_liste[i_n])
  i_n += 1

# wenn wir einem Listenelement einen neuen Wert eines anderen Datentyps
# zuweisen, ändert auch das Listenelement seinen Datentyp
q_liste[0] = q_liste[0] + q_liste[1]
print('Datentyp: ', type(q_liste[0]), ' Datenwert= ', q_liste[0])

Hinweis für Programmiererfahrene

Bei Kenntnis anderer Programmiersprachen "fühlen" sich diese gemischten Listen i.d.R. ungewohnt an, weil man gewohnt ist, dass in listenartigen Datenstrukturen alle Elemente gleichen Datentyps sind und man damit "rechnen" kann. Wir könnten zwar auch mit den Standard-Listen aus Python mathematische Rechnungen durchführen (die wir selber implementieren müssten), tatsächlich aber gibt es dafür in Python ein spezielles Modul mit Namen NumPy (für "Numerical Python"), das neben Datentypen für Vektor- und Matrizenrechnungen viele weitere mathematische Funktionen bereitstellt. (Die Funktionalität von NumPy ist stark an MATLAB angelehnt.)

Bisher haben wir unsere Listen immer auf einen Schlag angelegt und dann auf die Elemente zugegriffen. Was machen wir aber, wenn wir im Programmverlauf eine Liste erweitern oder verkleinern wollen?

 1list_1 = [1,2,3]
 2print(list_1)
 3
 4# mit der Funktion append() können wir EIN neues Element hinten anfügen
 5list_1.append(4)
 6print(list_1)
 7
 8# mit der Funktion insert() fügen wir ein Element an einer Position ein
 9list_1.insert(2, 5) # wir fügen das Element 5 an Index 2 (also an 3. Stelle) ein
10print(list_1)
11
12# mit der Funktion remove() können wir ein Element aus der Liste entfernen
13list_1.remove(3)  # wir entfernen das erste Element mit dem Wert 3 aus der Liste
14print(list_1)
15
16# wenn bei remove() der Wert nicht in der Liste enthalten ist sieht es so aus:
17list_1.remove(7)  # wir entfernen das erste Element mit dem Wert 7
18print(list_1)
19
20# Neben den hier aufgeführten Funktionen, gibt es viele weitere, die auf
21# Listen arbeiten.

Notation

Bei Verwendung der Funktionen print() oder len() war der Aufruf bisher immer in der Form:

# rückgabewert und argumente sind optional
rückgabewert = funktionsname(argument_1, argument_2, argument_3,...)

Im letzten Codeausschnitt sah die Notation für den Funktionsaufruf aber anders aus:

# rückgabewert und argumente sind optional
rückgabewert = variablenname.funktionsname(argument_2, argument_3,...)

Diese Aufrufform entspricht der sog. objektorientierten Notation, bei der die Funktionen (oft auch "Methoden" genannt) auf Datenobjekten/Variablen arbeiten. Für jeden Datentyp werden dabei die für ihn sinnvollen Funktionen implementiert und wir können diese Funktionen dann für Datenelemente des entsprechenden Typs nach o.a. Notation aufrufen. Inhaltlich entspricht die o.a. zweite Form dem (nicht-objektorientierten) Funktionsaufruf:

# rückgabewert und argumente sind optional
rückgabewert = funktionsname(variablenname, argument_2, argument_3,...)

Das Objekt bzw. die Variable mit der wir die Funktion objektorientiert aufrufen hat also die Rolle des ersten Arguments inne.

Auch wir selber werden später lernen, wie wir eigene Datentypen ("class") entwickeln und eigene Funktionen für diese sog. Klassen schreiben können.

Um zu vermeiden, dass eine exception auftritt, wenn der o.a. Code in den Zeilen 17/18 ausgeführt wird, weil das zu entfernende Element gar nicht in der Liste enthalten ist, können wir dies auch vorher abtesten:

# ... vorheriger Code

# diese Zeilen ersetzen die Zeilen 17/18
if 7 in list_1:
  list_1.remove(7)  # wir entfernen das erste Element mit dem Wert 7
  print(list_1)

for-Schleifen

Nachdem wir nun Listen als Datentyp kennengelernt haben, haben wir auch genug Vorwissen um eine weitere sehr nützliche Schleifen-Form neben der while-Schleife kennenzulernen.

... Anweisungen vor der 'for'-Schleife

# am Ende der for-Anweisung muss wieder ein ':' stehen
for i in Liste:
  # i nimmt nacheinander alle Werte in der Liste an
  # dieser eingerückte Progammabschnitt wird solange ausgeführt, wie Werte
  # in der Liste sind
  ... tue dies
  ... und noch mehr

# wenn die Einrückung zu Ende ist, geht der Progammablauf nach dem Ende der Schleife hier weiter
... Anweisungen nach der 'for'-Schleife

Wir schauen uns zunächst ein paar Beispiele an:

#------------------------
print('Liste fest vorgegeben:')
for i in [0,1,2,3,4,5,6]:
  print(i)
#------------------------
print('Liste als Variable:')
list_1 = [0,1,2,3,4,5,6]

print('Schleife über alle Elemente der Liste:')
for i in list_1:
  print(i)

print('Schleife bis zum 3.Element der Liste:')
for i in list_1[:3]:
  print(i)

print('Schleife über jedes 2.Element in der Liste:')
for i in list_1[::2]:
  print(i)

print('Schleife über jedes 2.Element in der Liste von hinten:')
for i in list_1[::-2]:
  print(i)
#------------------------

So nützlich die for-Schleife uns erscheint, so aufwendig wäre es, wenn wir für große Schleifen händisch eine entsprechend große Liste anlegen müssten. Glücklicherweise gibt es auch hier eine Alternative in Python, die uns das Leben leichter macht:

#------------------------
print('Schleife von 0..99 unter Nutzung von range()')
for i in range(0,100):
  print(i)
#------------------------
# Natürlich können wir mit range() auch zunächst eine Liste
# erzeugen und dann die Schleife über die Liste laufen lassen
list_2 = range(0,100,10)
print('Schleife über alle Elemente von list_2')
for i in list_2:
  print(i)

# Allgemeine Form:
# range(Startwert, Endwert+1, Schrittweite)
#------------------------

Aufgabe 3.1

Wir greifen die Berechnung der Sinus-Funktion wieder auf, wollen dieses Mal aber keinen einzelnen Wert berechnen. Vielmehr wollen wir die Funktionswerte für ein ganzes Intervall berechnen und uns im nächsten Schritt ausgeben lassen.

To Do

Es soll im ersten Schritt ein Programm erstellt werden, dass den Sinuswert für einen Reihe von Stützpunkten berechnet und in Form einer Liste abspeichert:

  1. das Werteintervall soll \(0...2\pi\) beigetragen

  2. es sollen \(n=100\) Stützpunkte berechnet werden

  3. sowohl das Argument \(x\) wie das Ergebnis \(sin(x)\) sollen in jeweils einer Liste abgelegt werden

  4. die Berechnung soll mithilfe einer for-Schleife erfolgen

  5. nach Berechnung aller Werte sollen die Wertepaare \((x, sin(x))\) zeilenweise auf der Konsole ausgegeben werden

Lösungshinweise:

  • leere Listen werden mit = [] erzeugt

  • anhand der Anzahl der Stützpunkte und der Größe des Werteintervalls lässt sich eine Schrittweite berechnen, um die das Argument von Berechnung zu Berechnung wachsen muss

  • es empfiehlt sich, sowohl das Werteintervall wie die Anzahl der Stützpunkte nicht "hart" zu codieren, sondern in Variablen abzulegen

Beispiel 3.2

Dateiausgabe

Während die Konsole für die Ein- und Ausgabe "einfach da ist", müssen Dateien explizit für die Arbeit mit ihnen geöffnet und geschlossen werden:

 1# Datei öffnen
 2fd = open('datei_1.txt','w')    # open(filename, mode)
 3print('Dateiname: ', fd.name)
 4
 5# mit write() können wir einen STRING in die Datei schreiben
 6fd.write('Eine sinnfreier Text')
 7# ... und noch einen
 8fd.write('Eine sinnfreier Text')
 9# ... und einer geht noch
10fd.write('Eine sinnfreier Text')
11
12# Datei schließen
13fd.close()

In dem vorstehenden Programm nutzen wir die "normale" Funktion open() um die Datei zu öffnen. Bei erfolgreichem Öffnen (wenn etwas schiefgeht wird eine exception ausgelöst) enthält die hier als fd bezeichnete Variable ein Objekt mit dem wir Dateifunktionen (in objektorientierter Notation) ausführen können. Mit write(string) schreiben wir dann den gleichen String dreimal nacheinander in die Datei und am Ende schließen wir diese wieder.

Wenn wir nun die Datei im Editor öffnen und sie uns anschauen, sehen wir, dass alle drei Strings in eine Zeile geschrieben wurde. Ein Zeilenumbruch hat also nicht stattgefunden. Anders als die uns von der Konsole bekannte print()-Funktion gibt write() nicht automatisch am Ende einen Zeilenvorschub aus, vor allem aber verarbeitet write() in diesem Fall wirklich nur Zeichenketten, wie wir in diesem Fall sehen:

# Datei öffnen
fd = open('datei_1.txt','w')
print('Dateiname: ', fd.name)

# Versuch mit write() einen Integerwert in die Datei zu schreiben
fd.write(42)
# ... hierhin kommen wir schon gar nicht mehr

# Datei schließen
fd.close()

Wir erinnern uns, dass wir beim Einlesen von der Konsole schon einmal den umgekehrten Fall hatten: input() lieferte uns einen String, den wir mit float(input()) in einen Float-Wert umgewandelt haben - sofern der eingegebene String in eine Zahl umwandelbar war. Analog gibt es auch eine Funktion mit der wir aus einem Zahlenwert einen String machen können: str(Zahlenwert):

# Datei öffnen
fd = open('datei_1.txt','w')
print('Dateiname: ', fd.name)

# Versuch mit write() einen Integerwert in die Datei zu schreiben
fd.write(str(42))
# ... hierhin kommen wir schon gar nicht mehr

# Datei schließen
fd.close()

Wir können aber der Funktion write() als Argument auch einen z.B. mittels '+' erzeugten String aus mehreren Bestandteilen übergeben. Und insbesondere können wir im String auch sog. Formatanweisungen übergeben: '\n' löst einen Zeilenumbruch aus, '\t' entspricht einem Tabulatorb (es gibt noch weitere Formatanweisungen für Strings):

# Datei öffnen
fd = open('datei_1.txt','w')
print('Dateiname: ', fd.name)

# eine Zeile mit einem Zahlenwert
fd.write(str(42)+'\n')
# eine Zeile mit einem komponierten String
fd.write(str(3.1415)+'\t'+'gleich kommt einen Zeilenumbruch\n')

# Datei schließen
fd.close()

Damit haben wir nun genug Wissen, um zumindest etwas in eine Datei schreiben zu können. Den umgekehrten Weg, das Einlesen von Daten aus einer Datei, werden wir uns etwas später anschauen.

Merke

Bei Dateien unterscheidet man primär zwischen zwei unterschiedlichen Bearbeitungsformen: Textdateien und Binärdateien. Textdateien sind grundsätzlich für uns Menschen lesbar, während Binärdateien codierte Informationen enthalten und nur mit den entsprechenden Codier-/Decodierverfahren geschrieben und gelesen werden können (z.B. Office-Dateien im *.docx oder *xlsx Format). Die Funktionen zur Dateibearbeitung in Python sind für beide Formate die selben, wir müssen Python aber mitteilen in welchem Format die Datei sein soll. Dazu gibt es im Aufruf der Funktion open(filename, mode) den Parameter mode, der u.a. folgende Werte annehmen kann:

  • 'wt' (oder 'w'): Textdatei zum Schreiben öffnen

  • 'rt' (oder 'r'): Textdatei zum Lesen öffnen

  • 'wb': Binärdatei zum Schreiben öffnen

  • 'rb': Binärdatei zum Lesen öffnen

Aufgabe 3.2

In Fortsetzung der Aufgabe 3.1. wollen wir unsere neu gewonnenen Erkenntnisse über den Umgang mit Dateien gleich in die Tat umsetzen.

Aufgabe 3.2.1

To Do

Das in Aufgabe 3.1 entstandene Programm ist so zu modifizieren, dass:

  1. die Ausgabe der Listen für x und sin_x in eine Textdatei geschrieben wird - jedes Wertepaar (x, sin_x) in einer separaten Zeile

  2. der Dateiname soll von den Nutzern erfragt werden

  3. nach dem Schreiben der Datei eine Meldung folgender Form auf der Konsole ausgegeben wird: "n Datensätze in der Datei filename gespeichert" (n und filename sind durch die aktuellen Werte zu ersetzen)

  4. die Ausgabe der Listen auf der Konsole soll im Gegenzug entfallen

Nach der Aufgabe kontrollieren wir das Ergebnis unserer Dateiausgabe - wenn alles in Ordnung ist, sollte unsere Datei über 100 Zeilen mit 2 Spalten verfügen.

Aufgabe 3.2.2

Unser Programm aus Aufgabe 3.1.1 soll nun etwas flexibler werden indem wir einige Parameter von den Nutzern abfragen.

To Do

Das Programm aus Aufgabe 3.1.1 soll um folgende Merkmale ergänzt werden:

  1. Abfrage des Namens für die Ausgabedatei

  2. Abfrage der Anzahl an Stützpunkten für die Sinus-Funktion