Python-Kurs

Lektion 1

Grundlagen
 
Hello World -Programm: Konsole öffnen und python eintippen. Es öffnet sich der interaktive Python-Interpreter. Dort eingeben: 
print("Hello World!")
und <ENTER> drücken.
 
Hello World als Skript: Öffne im Editor eine Datei hello.py , print-Befehl wie oben in die Datei schreiben und speichern. Dann in der Konsole eingeben:
python hello.py
 
Fortgeschrittene Version: Hello, <user>!
name = input("Wie heisst du? ")
print("Hello", name)
 
Wie funktioniert Python prinzipiell?
Der Prozessor des Computers kann nur Maschinensprache ausführen, Menschen bevorzugen die Programmierung in einer "Hochsprache" wie Python oder C++, wegen der besseren Lesbarkeit. Die Hochsprache muss vor der Ausführung in die Maschinensprache übersetzt werden. Zwei Ansätze:
  • offline: Kompilation in ein ausführbares Programm ("Executable"): C, C++, Fortran 
  • online: (unmittelbar vor der Ausführung): Python und generell interpretierte Sprachen bzw. Skriptsprachen. D.h., wenn man Python auf der Kommandozeile startet, läuft im Hintergrund ein Programm, das die Eingaben in Maschinensprache übersetzt und sofort ausführt.
  • Vorteil: flexibel - z.B.: man kann das Programm während der Ausführung verändern (oder überhaupt erst schreiben)
  • Nachteil: langsamer
 
Wie kann man Python ausführen?
  • Skript schreiben und auf der Kommandozeile python skript.pyaufrufen
  • python ohne Argument: startet den interaktiven Interpreter, dann kann man die Kommandos eintippen
  • bequemere Kommandozeile: ipython 
  • command? ruft die Hilfe auf
  • Pfeil hoch/runter ruft die vorigen Kommandos zurück
  • usw.
  • spyder in der Kommandozeile aufrufen: enthält ipython rechts unten, einen Skript-Editor links, den Variable-Inspektor (oben rechts) zum Ansehen der aktuell definierten Variablen sowie den Object Inspector (oben rechts) zum Anzeigen von Hilfe mittes CTRL-I. 
  • spyder ist eine IDE (Integrated Development Environment) ähnlich zu Matlab
  • Alternativen: pycharm, pydev-plugin für Eclipse, Python Tools for Visual Studio, kdev-python for KDevelop
  • jupyter notebook startet einen Python-Webserver auf dem lokalen Computer. Mit New => python3 kann man ein neues Notebook erzeugen und dort in den "Zellen" Code einfügen und mittels SHIFT + Enter sofort ausführen.
 
Jedes Python Programm besteht aus eigenem und fremden Code. Fremder Code wird normalerweise in Modulen angeboten, die man zunächst importieren muss.
import sys    # importiere das Modul 'sys'
sys enthält Informationen über das aktuelle System:
print(sys.platform)
Man kann das Inhaltsverzeichnis eines Modules mit dir(modulename) anzeigen:
dir(sys)
In ipython kann man das vereinfachen, indem man sys.<TAB> eingibt, oder sys.anfang<TAB> zur Vervollständigung.
Das __builtin__ -Modul ist immer vorhanden und stellt grundlegende Funktionalität zur Verfügung:
dir(__builtin__)
dir() ohne Argumente gibt aus, welche Variable und Funktionen zur Zeit vorhanden sind.
Das Modul pip stellt die Python-Module zur Verfügung:
 import pip
 pip.get_installed_distributions() # welche Module
                           # kann man importieren?
 
Was macht ein Programm eigentlich?
Jedes Programm bildet Eingaben auf gewünschte Ausgaben ab, d.h. es manipuliert den Speicher des Computers. Programme in Hochsprachen manipulieren den Speicher nicht direkt, sondern mittels 'Variablen'. Variablen haben
  • einen Namen ("Identifier"): besteht aus einer Zeichenfolge der Buchstaben A-Z, a-z (keine Umlaute!), Ziffern 0-9 und _, dürfen aber nicht mit einer Ziffer beginnen
  • einen Typ, der festlegt, wie der unterliegende Speicher manipuliert wird ("Interpretation des Speichers"). Typen bieten Funktionen und Operatoren an, mit denen man den Speicher auf genau definierte Art manipulieren kann
  • den zugehörigen Speicherbereich
Man kann den Typ mit der type()-Funktion abfragen:
 type("Hello") # str
 type(1)       # int
 type(1.0)     # float
 type(True)    # bool
 type(None)    # NoneType
 
 
 
Um eine Variable von einem bestimmten Typ zu erzeugen, kann man 
  • für grundlegende Typen ein "Literal" angeben - ausgeschriebene Konstanten eines bestimmten Typs
 "Hello", 'Hello'  # String-Literal
 1                 # Integer-Literal
 1.0, 1e-12        # float-Literale
 True, False       # bool-Literale
  • den Konstruktor aufrufen: spezielle Funktion, die so heisst wie der Typ selbst und ein neues Objekt dieses Typs erzeugt:
int(1)
float(1)
int()  # => 0
str()  # => ""
Python hat "automatische Speicherverwaltung", d.h. der Speicher wird der Variablen bei der Konstruktion zugewiesen und automatisch wieder freigegeben, sobald die Variable nicht mehr benötigt wird. Python stellt dies fest durch "Referenzzählung" und "garbage collection" (später ?).
 
Um der Variablen einen Namen zu geben, wird der Zuweisungsoperator = verwendet:
    a = 1   # Variable 'a' hat Typ 'int', Wert '1'
 
Arithmetik und Algebraische Funktionen:
 
Taschenrechner-Funktionalität: + - * / 
 
a = 2*300
b = a - 4
 
Besonderheit: es gibt zwei Operatoren für Division
  • /:  'exakte' Division, gibt immer eine reelle Zahl zurück (Typ float)
  • //: abrundende Division ('floor' division): rundet auf die nächste ganze Zahl ab
Man kann zwischen ganzen und reellen Zahlen umwandeln durch Konstruktoraufruf
a = -2.6
type(a)    # float
b = int(a) 
print(b)   # -2: Typumwandlung rundet Richtung 0
type(b)    # int
c = float(b)   # type(c) => float
 
Aufgabe 1:
gegeben zwei ganze Zahlen a und b, berechnen Sie den Rest der Division a // b (nicht mit Hilfe der vordefinierten Rest-Funktion!)
a = 10
b = 3
...
print(rest) # => 1
 
Die vordefinierte Rest-Funktion ist der Operator %. Man kann das Ergebnis der Berechnung mit dem assert-Befehl prüfen:
assert rest == a % b  # '==' fragt 'ist das gleich?'
                      #  ergibt 'True' oder 'False'
Tip: Schreibe Skript rest.py, wo am Anfang a und b initialisiert werden. Dann kann man das Skript mit verschieden Werten für a und b immer wieder ausführen: python rest.py.
 
Lösung:
a = int(input("Introduce value of a:"))
b = int(input("Introduce value of b:"))
 
rest = a - a//b * b
print(rest)
assert rest == a % b
 
Python unterstützt die Potenzierung mit dem ** Operator:
10**10   # '10 hoch 10'
10.0**10
 
Der int-Typ in Python kann beliebig große Zahlen speichern!
 
Operatorpriorität und Klammerregeln:  In Python geht Punkt- vor Strichrechnung. Arithmetische Operatoren  haben höhere Priorität als Vergleichsoperatoren, diese wiederum höhere  als logische Operatoren (and, or). Der Potenzoperator ** hat höhere Priorität als die Punktrechnung, ein Funktionsaufruf funktion(arg) die höchste. Wenn man die Berechnungsreihenfolge ändern will, kann man runde Klammern verwenden:
2*3+5   # => 11
2*(3+5) # => 16
Operatoren gleicher Priorität (+, - sowie *, /, //, %) werden von links nach rechts ausgeführt.
 
Algebraische Funktionen befinden sich im Modul math 
import math
math.sqrt(2.0)  # Modulnamen vor Funktionsnamen 
                # schreiben, mit Punkt getrennt
 
from math import sqrt  # Funktion sqrt ins aktuelle
                       # Modul importieren
sqrt(2.0)
 
dir(math)   # => die üblichen Funktionen: exp, ...
 
Ausnahmen: die abs()-Funktion (Absolutbetrag) und die round()-Funktion (Runden zur nächsten ganzen Zahl) befinden sich im Modul __builtin__ und müssen nicht importiert werden. Hingegen befinden sich die floor()- und ceil()-Funktionen (Ab- und Aufrunden) im Modul math.
 
Vergleichsoperatoren: 
  • a == b: haben a und b den gleichen Wert?
  • a != b: haben a und b ungleiche Werte?
  • a < b, a <= b, a > b, a >= b entsprechend
  • ergeben boolsche Variable True oder False 
Logische Verknüpfungen zwischen boolschen Werten
  • a and b, a or b, not a 
Verzweigungen: if: ... elif: ... else: ... 
if a == 0:    # der Doppelpunkt ist wichtig!
    b = 3
    c = 4
    print(a, b, c)
elif a == 1:
    b = 5
    c = 1
else:
    b = 10
    c = 0
print(a, b, c)
 
Wichtig: in Python trägt die Einrückungstiefe Bedeutung! Gleich tief eingerückte Befehle gehören zusammen. In anderen Programmiersprachen gibt es dafür explizite Syntax, z.B. if ... endif, begin ... end, { ... }, die in Python einfach wegfallen. Wichtig ist aber: Leerzeichen und Tabzeichen sind unterschiedliche Einrückung, auch wenn sie im Editor gleich aussehen. 
Tipp: den Editor so einstellen, dass beim Drücken der Tab-Taste automatisch 4 Leerzeichen eingefügt werden, statt eines Tab-Zeichens.
 
Aufgabe 2:
Gaußsche Wochentagsregel: Datum eingeben, zugehörigen Wochentag ausgeben
 
  • Eingabe: day, month, year mittels 
day = int(input('Tag: '))
...
  • Die Monatszählung für den Algorithmus beginnt bei März = 1, Januar und Februar sind die Monate 11 und 12 des vorhergehenden Jahres => als ersten Schritt die üblichen Monatsnummern in diese Schreibweise umwandeln (neue Variablen m und year1 in einer  if-Anweisung definieren)
  • Formel: siehe Tafel 
  • Ausgabe am Ende: Der <Datum> war ein <wochentag> (Diese Forderung ist verbindlich! Wenn Ihr Programm in Klausur oder Übung nicht exakt das geforderte ausgibt, gibt es Punktabzug.)
  • Skript wochentag.py (Der Name des Skripts ist ebenfalls verbindlich!) 
  • Testen Sie mit verschiedenen Daten, die Sie im Internet nachschlagen können.
 
Lösung: 
day = int(input("Tag: "))
month = int(input("Monat: "))
year = int(input("Jahr: "))
 
m = month - 2 
if m < 2:
        m += 12
        year -= 1
year1 = year % 100
c = year // 100
w = (day + (26*m - 2)//10 + year1 + year1//4 + c//4 - 2*c) % 7
 
if w<0:
        w += 7
 
if w == 0:
        w = "Sonntag"
if w == 1:
        w = "Montag"
if w == 2:
        w = "Dienstag"
if w == 3:
        w = "Mittwoch"
if w == 4:
        w = "Donnerstag"
if w == 5:
        w = "Freitag"
if w == 6:
        w = "Samstag"
 
print("Der", day, month, year, " war ein", w)
 
 
Eigene Funktionen schreiben:
# der Doppelpunkt am Ende ist wichtig!
def funktionsname(argument1, argument2, ...):
    '''Dokumentation
       Drei Anführungszeichen: mehrzeilige Strings (
       dürfen Newlines enthalten)
    '''
    code
    code
    return ... # Ergebnis zurückgeben
 
Aufgabe 3:
wochentag.py umschreiben mit Funktion
def day_of_week(day, month, year):
   ...
   return weekday  # als String
 
Der übrige Code (input, print) bleibt am Ende des Skripts, die neue Funktion kommt an den Anfang.
 
Lösung:
def day_of_week(day, month, year):
        m = month - 2
        if m < 2:
                m += 12
                year -= 1
        year1 = year % 100
        c = year // 100
        w = (day + (26*m - 2)//10 + year1 + year1//4 + c//4 - 2*c) % 7
 
        if w<0:
                w += 7
        if w == 0:
                w = "Sonntag"
        if w == 1:
                w = "Montag"
        if w == 2:
                w = "Dienstag"
        if w == 3:
                w = "Mittwoch"
        if w == 4:
                w = "Donnerstag"
        if w == 5:
                w = "Freitag"
        if w == 6:
                w = "Samstag"
 
        return w
day = int(input("Tag: "))
month = int(input("Monat: "))
year = int(input("Jahr: "))
 
print("Der", day, month, year, " war ein", day_of_week(day, month, year))
 
Eigene Module: Skripte, die Funktionen definieren, können als Module verwendet werden, indem die Datei mittels import in eine andere Datei importiert wird.
 
Aufgabe 4:
Schreiben Sie ein Skript run_wochentag.py, das am Anfang import wochentag aufruft. Dann steht die Funktion aus Aufgabe 3 als wochentag.day_of_week zur Verfügung. Alternativ kann man auch from wochentag import day_of_week aufrufen, dann ist die Funktion direkt als day_of_week aufrufbar. Alle Funktionalität außer der Funktion day_of_week (also input, print etc.)  soll nach run_wochentag.py verschoben werden. Dann sollte python run_wochtag.py funktionieren wie zuvor python wochentag.py. Vorteil: die Funktion day_of_week ist jetzt in anderen Programmen wiederverwendbar.
 
Eine Python-Datei kann gleichzeitig Modul und Skript sein. Das ist besonders nützlich zum Testen: Als Modul stellt die Datei Funktionen zur Verfügung, als Skript testet sie diese Funktionen:
# Modul-Teil
def funktion1():
    ...
    
def funktion2(arg):
    ...
    
# Skript-Teil: die Bedingung ist nur True, wenn die 
# Datei als Skript aufgerufen wurde
if __name__ == '__main__'
    ... # Testcode
    
    print("All tests succeeded")
 
Dann kann man aufrufen
python wochentag.py     # Testen
python run_wochentag.py # enthält 'import wochentag': Wochentage ausrechnen 
 
Aufgabe 5:
In wochentag.py Tests hinzufügen.
 
Syntax für assert:
assert <Bedingung, die True sein muss>, "optionale Fehlermeldung"
 
Im Wochentagsbeispiel:
assert wochentag(4, 4, 2016) == "Montag"
 
Wie findet man gute Testbeispiele?
  • Tests sollen alle Zweige eines Programms abdecken. Z.B. bei jedem if sollte mindestens einmal True und einmal False getestet werden. Hier: Teste Januar/Februar und die übrigen Monate
  • Der Test muss typische Fälle und Randfälle testen. Typisch wäre ein harmloses Datum wie 4.4.2016. 
  • Randfälle: 
  • Schaltjahre, keine Schaltjahre
  • Schalttage und Tage unmittelbar davor oder danach
  • Der 1. Januar und der 31. Dezember
  • Schaltjahre, die aufgrund von Ausnahmen doch keine Schaltjahre sind: 1900, 2100
  • aber: es gibt auch welche, die wegen einer Ausnahme von der Ausnahme doch wieder Schaltjahre sind: 2000
 
Vorbedingungen und Exceptions:
Die meisten Funktionen dürfen nur ausgeführt werden, wenn bestimmte Bedingungen erfüllt sind. Hier: Die Eingabe muss ein gültiges Datum sein.
 
Aufgabe 6:
Schreibe eine Funktion, die prüft, ob die Eingabe ein gültiges Datum ist, inklusive der Schaltjahre und Ausnahmen
def isValidDate(day, month, year):  # => True/False
    if day < 1 or day > 31:
        return False
    ...
Die Funktion sollte im Testteil der Datei wochentag.py ebenfalls getestet werden (je einige Beispiele, wo True bzw. False herauskommt, dabei besonders die Spezialfälle wie 29.2.2016 und 29.2.2017 beachten).
 
Vorbedingungen werden am Anfang der Funktion geprüft. Ist eine Bedinung nicht erfüllt, wird eine "Ausnahme" (exception) ausgelöst:
def weekday(day, month, year):
    if not isValidDate(day, month, year):
        raise ValueError("weekday(): kein gültiges Datum.")
    ... # Funktion normal weiter ausführen
 
Übungsblatt 1:
  • Fehler finden
  • Unterschiede int und float 
  • Gaußsche Osterregel
 

Lektion 2

Schleifen
 
Schleifen sind der Kern der Programmierung, weil Computer nur dann nützlich sind, wenn man das gleiche sehr oft wiederholen muss. Sonst kommt man händisch meist schneller zum Ziel. 
 
while-Schleife: funktioniert im Prinzip wie if, aber nach Ausführung des Schleifenkörpers wird auf die while-Anweisung zurückgesprungen:
k = 0
while k < 10:
    print(k)
    k = k + 1  # wer das vergisst, bekommt eine 
               # Endlosschleife
Für k = k + 1 kann man abkürzend schreiben: k += 1.
 
Aufgabe 1:
Modifiziere die Schleife so, dass nur die geraden Zahlen ausgegeben werden.
 
Drei Möglichkeiten:
 
Ungerade Zahlen überspringen
k = 0
while k < 10:
    print(k)
    k = k + 2  # ungerade Zahlen überspringen
 
Gerade Zahlen mit if auswählen
k = 0
while k < 10:
    if k % 2 == 0: # keine Ausgabe für ungerade Zahl
        print(k)
    k = k + 1 
 
Schleifenkörper mit continue vorzeitig beenden. Vorsicht: dabei vergisst man oft, die Schleifenvariable zu inkrementieren => Endlosschleife. Hier führt diese Lösung zu hässlichem Code.
k = 0
while k < 10:
    if k % 2 != 0:
        k += 1
        continue  # Schleifenkörper vorzeitig 
                  # abbrechen, zum while zurück
    print(k)
    k = k + 1 
 
Anwendung: Näherungsweise Berechnug der Quadratwurzel:
 
def my_sqrt(x):
    y = x / 2
    while y*y != x:
        y = (y + x/y) / 2
    return y
 
Aufgabe 2:
Dies in einem Modul wurzel.py implementieren, und im Skriptteil des Moduls Tests schreiben.
 
Exceptions sind spezielle return-Werte für Fehlerfälle. Sie werden statt mit return mit raise zurückgegeben. Im Unterschied zu return wird eine Exception von der aufrufenden Funktion nicht einfach empfangen, sondern die aufrufende Funktion wird ebenfalls mit der Exception beendet, die Exception wird also "weitergereicht".
 
Lösung:
import sys
 
# Funktion mit default-Wert für Argument 'eps'
def close_at_tolerance(a, b, 
                       eps=sys.float_info.epsilon):
    '''Tests if a and b are almost equal, 
       with relative error eps.
    '''
    diff = abs(a - b)
    
    # absoluter Fehler wäre
    # return diff <= eps
       
    # relativer Fehler
    return diff <= eps * max(abs(a), abs(b))
 
def my_sqrt(x):
    if x < 0:  # precondition check
        raise ValueError("my_sqrt(): negative argument")
        
    y = x / 2 # initial guess
    # while y*y != x:
    # dieser einfache Test funktioniert nicht für 
    # 'krumme' Zahlen (endliche Genauigkeit 
    # von 'float' - relativer Fehler 2e-16)
    while not close_at_tolerance(y*y, x, sys.float_info.epsilon):
        y = (y + x/y) / 2
 
    return y
    
if __name__ == "__main__":
    assert my_sqrt(9) == 3
    assert my_sqrt(0) == 0
    
    assert my_sqrt(4) == 2
    
    # Exception abfangen und behandeln
    # => so testet man, dass Exceptions 
    # wie gewünscht funktionieren
    try:  # "probiere etwas, was eine 
          #  Exception auslösen könnte"
        my_sqrt(-1)
        # wenn die Exception wie gewünscht ausgelöst
        # wurde, ist die folgende Zeile unerreichbar
        assert False, "my_sqrt(-1) didn't raise ValueError"
    except ValueError as e: # Fange die Exception 
                            # 'ValueError' ab
                            # 'as e' speichert die 
                            # Exception in der 
                            # Variablen e
        pass  # nichts tun, alles in Ordnung
        # print(e) würde die Fehlermeldung ausgeben
    # andere Exceptions werden weitergereicht
        
    
    assert my_sqrt(1.21) == 1.1
    assert close_at_tolerance(my_sqrt(0.5), 1.0 / my_sqrt(2.0))
    
    print("All tests passed.")
 
Mächtiger als die while -Schleife ist die for-Schleife:
for k in <iterable>:
    ... # Schleifenkörper
 
iterable ist alles, worüber man iterieren kann => Details später
 
for k in (0, 1, 2, 3):
    print(k)
 
Der Ausdruck (0, 1, 2, 3) erzeugt ein tuple-Objekt mit den gegebenen Werten:
t = (0, 1, 2, 3)
print(type(t)) # => tuple
print(t)       # => (0, 1, 2, 3)
print(t[1]) # => 1 - Zugriff auf einzelne Element 
            # mit Index-Operator [index]
 
Die for- Schleife belegt k nacheinander mit den Elementen des Tupels.
 
Aufgabe 3:
Nur die ungeraden Zahlen ausgeben.
 
Nachteil des Tuples: man kann die Obergrenze der Iteration nicht einfach als Variable angeben => verwende die range() Funktion.
 
# range(start, stop, step)
# start und step sind optional 
# (wenn man step weglässt, gilt step = 1,
#  wenn man nur stop angibt, gilt start = 0
for k in range(4):
    print(k)  # gibt 4 Elemente aus: 0, 1, 2, 3
 
Beachte: Der stop-Parameter von range ist die erste Zahl, die nicht mehr ausgegeben wird. Da die Zählung bei 0 beginnt, werden dadurch exakt stop Zahlen ausgegeben bzw. Iterationsschritte ausgeführt.
 
for k in range(4, 0, -1): # (start, stop, step)
    print(k)  # gibt 4, 3, 2, 1 aus
 
Aufgabe 4:
Implementiere zwei Funktionen für die Fakultät x!, einmal mit while-Schleife, einmal mit for-Schleife und range.
 
Lösung:
def factorialWhile(x):
        i = 1
        res = 1
        while i <= x:
                res *= i
                i += 1
        return res
 
def factorialFor(x):
        res = 1
        for i in range(1, x+1):
                res *= i
        return res
 
print(factorialWhile(6))
print(factorialFor(6))
assert factorialWhile(15) == factorialFor(15)  # the two different implementations
                                               # should give the same result
 
Aufgabe 5:
Zwei Funktionen schreiben, die für einen gegebenen String einen String erzeugen, der
  1. nur jedes zweite Zeichen enthält
  1. den ursprünglichen String rückwärts enthält
Dann einen String mit input einlesen und die zwei neuen Strings erzeugen und am Ende ausgeben.
Hinweise:
  • leeren String erzeugen: s = "" oder s = str() 
  • an einen String etwas anhängen: s = s + "z" bzw. abgekürzt s += "z" 
  • zwei Möglichkeiten, über einen String zu iterieren:
  1. mit range()-Funktion über die Länge des Strings
    l = len(s) # Länge des Strings = Anzahl Zeichen
    for k in range(len(s)):
        zeichen = s[k]  # k-tes Zeichen abfragen
        ... # mit 'zeichen' etwas tun
  1. direkt über den String iterieren (String ist "iterable")
    for zeichen in s: # alle Zeichen nacheinander 
                      # an 'zeichen' zuweisen
        ... # mit 'zeichen' etwas tun
 
Lösung:
mystring = input("Geben Sie Ihren String ein: ")
 
def secondLetter(string):
        new_str = str()
        for k in range(len(string)):
                if k % 2 != 0:
                        new_str += string[k]
        return new_str
 
def reverseString(string):
        new_str = str()
        for k in string:
                new_str = k + new_str
        return new_str
 
print("Jeder zweite Buchstabe vom String: ",secondLetter(mystring))
print("Der String rueckwaerts: ",reverseString(mystring))
 
Nützliche Verwendungen von tuple:
  • Funktionen mit mehreren return-Werten (z.B. Osterdatum: Tag und Monat)
   def easter(year):
       ...
       return day, month # erzeugt ein tuple 
                         # (day, month) und gibt 
                         # dieses zurück
  • Beim Aufruf der Funktion kann man das tuple automatisch wieder entpacken, indem man auf der linken Seite der Zuweisung zwei (oder mehr) Variablen angibt:
   day, month = easter(2016) # entpackt das tuple, 
                             # das von easter() 
                             # zurückgegeben wird
  • Trick, um zwei Variablen zu vertauschen:
     a, b = b, a  # 'swap' a <=> b
 
Der string-Typ  
Nützliche Funktionen (mit dir(str) abrufbar):
string[k:] # gibt den String ab Index k aus
string[:k] # gibt den String bis Index k-1 aus
string.count('x') # gibt an, wie oft x im String vorkommt
string.find('x') # gibt den Index an, an dem sich x im 
                 # String befindet (von links gezählt)
string.rfind('x') # Gegenstück zu '.find()' (von rechts 
                  # gezählt)
string.isalpha() # prüft, ob der String nur aus Buchstaben 
                 # besteht (liefert boolean)
string.isnumeric() # prüft, ob der String nur aus Zahlen 
                   # besteht (liefert boolean)
string.lower() # wandelt den String in Kleinbuchstaben um 
string.upper() # wandelt den String in Großbuchstaben um
string.split('x') # teilt den String bei allen 'x' und löscht x
                  # Ergebnis: eine Liste der Teilstrings zwischen
                  # den gelöschten 'x'
string.splitlines() # teilt mehrzeilige Strings (split am Zeilenende)
string.replace('x', 'y') # ändert alle 'x' in 'y' und gibt den 
                         # neuen String zurück
string.replace('x', 'y', 4) # ändert nur die ersten 4 
                            # Vorkommen von 'x'
string.replace('vorher', 'nachher')# ersetzt alle Vorkommen
                                   # des Teilstrings 'vorher'
 
Aufgabe 6:
Eine Funktion normalize_string() implementieren, die einen gegeben String "normalisiert", d.h. alles in Kleinbuchstaben wandelt und Leerzeichen und Satzzeichen löscht.
 
Hinweis 1: Strings sind in Python 'immutable', d.h. können nicht geändert werden. Deshalb kann man die Aufgabe nur lösen, indem man schrittweise einen neuen, normalisierten String aufbaut (z.B. mittels normalised += next_character).
 
Hinweis 2: Um zu testen, ob ein Zeichen zu einer vorgegebenen Menge gehört (hier: Leerzeichen oder Satzzeichen), kann man den in-Operator verwenden:
if s[4] in " .?!,;":
    ... # tue etwas
if s[4] not in " .?!,;":
    .. # tue etwas
 
Lösung:
def normalize_string(string):
        normalised = str()
        for k in string:
                if k in (".?,;! "):
                        continue
                else:
                        normalised +=k.lower()
        return normalised
 
Aufgabe 7:
Benutzen Sie die Funktionen aus Aufgabe 5 und 6, um eine Funktion zu schreiben, die testet, ob ein gegebener String ein Palindrom ist, d.h. vorwärts und rückwärts gelesen werden kann. Bekannte Palindrome:
  • Anna
  • Radar
  • Lagerregal
  • Reliefpfeiler
Dann Strings einlesen und "ist Palindrom" oder "ist kein Palindrom" ausgeben.
  •  
Solution:
def is_palindrom(string):
        palindrom = normalize_string(string)
        return palindrom == reverseString(palindrom)
------------
Strings aus Dateien lesen und wieder speichern:
 
# Datei zum Lesen öffnen
infile = open("filename")
...  #  Daten einlesen
infile.close() # Datei wieder schließen (nicht vergessen!)
 
Damit man das Schließen nicht vergessen kann, hat Python den Befehl with:
# statt 'infile = open("filename")' schreibe
with open("filename") as infile: 
    ... # Daten einlesen (hier ist Datei offen)
pass # am Ende der Einrückung wird die Datei 
     # automatisch geschlossen, hier ist sie also zu
 
Gegenstück zum Schreiben der Datei:
# füge 'w' zum open()-Befehl hinzu
with open("filename", 'w') as outfile: 
    ... # Daten schreiben (hier ist Datei offen)
 
String-Daten aus der Datei lesen:
 
Version 1: alle Daten auf einmal
with open("filename") as infile: 
    contents = infile.read()
 
Version 2: zeilenweise
with open("filename") as infile: 
    # Zeilen in der Datei sind 'iterable', können 
    # als Argument der for-Schleife verwendet werden
    for line in infile.readlines(): 
        ... # bearbeite die aktuelle Zeile
 
String-Daten in die Datei schreiben:
 
Version 1: alle Daten auf einmal
with open("filename", 'w') as outfile: 
    outfile.write(contents)
 
Version 2: zeilenweise
with open("filename", 'w') as outfile: 
    outfile.writelines(lines)
 
Übungsblatt 2:
  • Sieb des Eratosthenes
  • Email-Adressen validieren
 

Lektion 3

 
Der Array-Typ list:
 
Array sind Sequenzen von beliebigen Elementen und mit beliebiger Länge, d.h. sie ähneln Strings, aber
  • Elemente müssen nicht Zeichen sein
  • die Elemente können nachträglich verändert werden
Erzeugen mit eckigen Klammern:
array = [1, 2.0, 3, '4'] # vier Elemente
leer  = []
auch_leer = list()
 
Erzeugen aus anderen Typen:
string_array = 'asdf asdjh asdkj'.split(' ')
# ergibt ['asdf', 'asdjh', 'asdkj']
 
a_tuple = (3, 4, 5, 6)
array_from_tuple = list(a_tuple)
# ergibt [3, 4, 5, 6]
 
zahlen = list(range(3, 0, -1)) # aus 'iterable'
# ergibt [3, 2, 1]
 
Man kann das auch schachteln. Der Zugriff erfolgt über den Index-Operator
nested = [leer, string_array, a_tuple, 1.0]
nested[0] # => []
nested[1] # => ['asdf', 'asdjh', 'asdkj']
nested[2] # => (3, 4, 5, 6)
nested[3] # => 1.0
 
Da Arrays mutable sind, kann man den Index-Operator auch auf der linken Seite der Zuweisung verwenden:
nested[2] = 'asdasd'  # a_tuple wird dadurch aus 
                      # dem Array entfernt
print(nested[2]) # => 'asdasd'
 
Aufgabe 1:
Erzeuge ein Array mit den Zahlen 0 ... 9 und schreibe zwei Funktionen, die das Array umdrehen
  1. indem ein neues, umgedrehtes Array erzeugt wird
gedreht = drehe(array)
assert gedreht != array
  1. indem das gegebene Array 'in place' gedreht wird (möglich, weil das Array 'mutable' ist)
drehe_in_place(array)
assert gedreht == array
 
Hinweis 1: Länge des Arrays abfragen
l = len(array)
Hinweis 2: Daten an Arrays anhängen
# zwei Arrays aneinanderhängen
array3 = array1 + array2  # neues kombiniertes Array erzeugen
array1 += array2          # array1 verlängern
# ein neues Element anhängen
array.append(single_element)
# äquivalent zu
array += [single_element] 
 
Lösung zu 1.1
# Funktion 'reversed' dreht das Array
gedreht = list(reversed(array))
 
Lösung zu 1.2
def drehe_in_place(array):
    i = 0               # erstes Element
    j = len(array) - 1  # letztes Element
    while i < j:
        # swap  i <=> j
        array[i], array[j] = array[j], array[i]
        i += 1  # von vorn zur Mitte
        j -= 1  # von hinten zur Mitte
oder als Einzeiler:
array.reverse()
 
Nützliche Funktionen (mit dir(array) abrufbar):
array.append(x) # hängt x am Ende des Arrays an
array.insert(idx, x) # fügt x beim Index idx ein
array += other_array # hängt other_array am Ende an
array.pop(idx) # löscht am Index idx den Eintrag
               # aus dem Array und gibt den
               # gelöschten Eintrag zurück (ohne
               # Argument wird der letzte Eintrag 
               # gelöscht)
del array[idx]  # löscht den Eintrag am Index idx, 
                # ohne ihn zurückzugeben
array.remove(x) # löscht das erste Vorkommen des 
                # Eintrages x aus dem Array
array.clear() # löscht alle Einträge aus dem Array
array.sort() # sortiert das Array aufsteigend
array.sort(reverse=True) # Sortierung absteigend
array.reverse() # dreht die Reihenfolge der Einträge um
array.copy() # erzeugt eine Kopie des Arrays
array.count(x) # gibt an, wie oft x im Array vorkommt
 
Aufgabe 2:
Die Funktion day_of_week mit Hilfe eines Arrays verbessern: Wir hatten
if weekday == 0:
     return "Sonntag"
if weekday == 1:
     return "Montag"
...
Dieser Code ist hässlich! Benutzen Sie ein Array, um die if zu eliminieren.
weekdays = ['Sonntag', 'Montag', ..., 'Samstag']
... # weekday ausrechnen ...
return weekdays[weekday] # und als Index benutzen
 
Lösung:
def day_of_week(day, month, year):
        m = month - 2
        if m < 2:
                m += 12
                year -= 1
        y = year % 100
        c = year // 100
        w = (day + (26*m - 2)//10 + y + y//4 + c//4 - 2*c) % 7
        weekdays = ["Sonntag", "Montag", "Dienstag", "Mittwoch",
                    "Donnerstag", "Freitag", "Samstag"]
        return weekdays[w]
 
Der Indexoperator der Python-Arrays ist sehr mächtig. 
  • grundlegende Verwendung: Lesen und Schreiben einzelner Elemente:
   value = array[2]       # Lesen
   array[2] = new_value   # Schreiben
  • negative Indizes sind erlaubt: sie werden vom Ende des Arrays aus gerechnet:
    value = array[-1] # Lesen des letzten Elements
    array[-1] = new_value # Schreiben des letzten Elements
    # array[-idx]  <=>  array[len(array)-idx]
  • Zugriff auf nicht existierendes Element (idx >= len(array) oder idx < -len(array)): index out of range error
  • Slicing-Syntax mit :-Operator: man kann auf ganze Subarrays zugreifen, nicht nur auf einzelne Elemente:
   subarray = array[2:5] # kopiert Array-Elemente von
     # Index 2 (inklusiv) bis Index 5 (exklusiv) in ein neues Array
   array[2:] # Endindex fehlt => kopiere bis zum Ende
   array[:5] # Anfangsindex fehlt => kopiere vom Anfang
   array[:] # kein Index => kopiere alles
  • mit zwei :-Operatoren kann man eine Schrittweite festlegen ('Stride'): 
   array[2:5:2] # nur jedes zweite Element kopieren
   array[::-1]  # alle Elemente rückwärts kopieren
  • die gleichen Indextricks funktionieren auch mit Strings => das ist sehr nützlich, um Teilstrings zu extrahieren, deren Grenzen man zuvor z.B. mit find() oder rfind() festgelegt hat.
   path = "/usr/lib/libpython.a"
   slash = path.rfind('/')
   filename = path[slash+1:] # => "libpython.a"
 
Warum ist append(x) (Einfügen am Ende) effizienter als insert(0, x) (Einfügen am Anfang, oder allgemein, nicht am Ende)?
=> Um n Elemente zu speichern, muss das Array Speicherplatz für n Elemente reservieren. Damit der Indexzugriff effizient ist, muss der Speicher zusammenhängend sein. Python reserviert mehr Speicher als zurzeit benötigt, damit neue Elemente am Ende angefügt werden können. Ist der reservierte Speicher verbraucht, reserviert Python einen doppelt so großen Bereich und kopiert das alte Array in die vordere Hälfte. Die Kopie kostet Zeit, aber man kann leicht beweisen, dass jedes Element im Mittel nur zwei Mal kopiert wird, wenn man die Speicherlänge jeweils verdoppelt. Das nennt man "dynamisches Array".
 
Wertsemantik und Referenzsemantik
 
Was bedeutet eine Variablenzuweisung b = a? Wír betrachten eine Analogie: wenn man auf eine Webseite zugreifen will gibt es zwei Möglichkeiten:
  • man hat die Webseite lokal kopiert: dann kann man einfach das File öffnen
  • man hat die URL gespeichert (z.B. als bookmark): Dann kann der Browser die Webseite auf Anforderung laden
Unterschied: Wenn ich die lokale Kopie ändere, hat es keinen Einfluss auf das Original. Wenn ich über die URL ändere, ändert sich das Original mit (falls ich Schreibrechte habe).
 
Anlegen einer Kopie entspricht Wertsemantik
Zugriff über URL entspricht Referenzsemantik
 
Python benutzt für die meisten Variablen Referenzsemantik:
a = [1, 2, 3]
b = a
b[0] = 'changed' # Referenzsemantik: a wird auch 
                 # geändert, weil 'a' und 'b' auf 
                 # den gleichen Speicher verweisen
assert a[0] == 'changed' 
Das heisst, nach der Zuweisung b = a ist b nur ein neuer Name für den gleichen Speicher wie a.
 
Für die grundlegenden Typen int, float, bool benutzt Python Wertsemantik. Nach der Zuweisung b = a enthält b zwar die gleiche Zahl wie a, aber mit eigenem Speicher.
a = 1
b = a
b = 2
assert a == 1 # Wertsemantik: Ändern von 'b' 
              # hat keinen Einfluss auf 'a' 
 
Aufgabe 3:
Wird für Strings Wert- oder Referenzsemantik verwendet? => Wertsemantik, weil Strings immutable sind.
 
Generell ist die Unterscheidung zwischen Wert- und Referenzsemantik nur relevant für Typen, die mutable sind, wie list, dict und eigene Klassen (haben?) (=> später), aber nicht für tuple und str.
 
Das Problem kann zu unangenehmen Überraschungen führen, wenn man nicht daran denkt, dass eine Variable nur ein neuer Name für eine andere Variable ist. Beispiel:
def do_something(array):
    ...
    array[2] = 5
    ...
    
a = [1,2,3,4]
do_something(a)  # Achtung: Referenzsemantik
print(a[2]) # ist das 3 oder 5 ? => 5
 
Man kann feststellen, ob zwei Variablen auf den selben Speicher verweisen, entweder mittels id()-Funktion oder mittels Vergleichsoperator is:
a = [1,2,3]
b = a
assert a == b  # Vergleich der Werte
assert a is b  # Vergleich der Speicherposition
assert id(a) == id(b) # Vergleich der Objekt-IDs
 
c = [1,2,3]
assert a == c      # Vergleich der Werte
assert not a is c  # Vergleich der Speicherposition
assert id(a) != id(c) # Vergleich der Objekt-IDs
 
Man kann für Arrays Wertsemantik erzwingen:
b = a[:]      # erzeugt eine Kopie
b = a.copy()  # erzeugt eine Kopie
Generell kann man eine Kopie erzwingen mit den Funktionen copy oder deepcopy aus dem Modul copy:
import copy
a = [1,2,3,4]
b = copy.deepcopy(a) # erzeugt eine Kopie
Die Unterscheidung zwischen 'deep' und 'shallow' copy ist wichtig => später ? oder Docu lesen !
 
Oft will man aus Arrays neue Arrays erzeugen, deren Elemente von den Elementen des alten Arrays abhängen.
Einfachste Lösung mit Schleife:
a = [1,2,3,4]
b = []
for e istrn a:
    b.append(e**2)
assert b == [1, 4, 9, 16]
 
Eleganter: funktionale Programmierung: Funktion map: transformiert ein Array in ein neues Array:
# definiere die Operation, die auf 
# die Elemente angewendet werden soll
def square(x):
    return x**2
 
b = list(map(square, a)) # führt obige Schleife aus
# beachte: in Python 3 gibt 'map' einen Iterator 
# zurück (zu Iteratoren später), den man erst in 
# ein Array umwandeln muss
 
Oft will man der Funktion nicht extra einen Namen geben: verwende 'namenlose' bzw. lambda-Funktionen
b = list(map(lambda x: x**2, a))
# 'lambda x:' bedeutet 'def unnamed(x):'
# 'lambda x, y:' bedeutet 'def unnamed(x, y):'
# beachte: bei 'lambda' gibt es kein 'return', es
# wird einfach der letzte Wert zurückgegeben
 
Aufgabe 4:
Implementiere die Transformation x => exp(-x/2) mittels map.
 
Lösung:
from math import exp
def myexp(x):    
        return exp(-x/2)
a = [2,3,4]
b = list(map(myexp, a))
 
Noch eleganter: list comprehensions. Das sind spezielle Listen-Konstruktoren, die während der Konstruktion eine Schleife mit einer Transformation ausführen.
# normaler Konstruktor
a = [1,2,3,4]
 
# list comprehension
b = [e**2            for e in a ]
#    ^ Element für b ^ Schleife für Originalelemente
#    ^ (beliebige Operation auf der Laufvariablen e)
 
Noch mächtiger: filtering list comprehensions: füge noch ein if ein, um nur Elemente nach b zu übernehmen, die eine bestimmte Eigenschaft haben
b = [e**2        for e in a   if e % 2 == 0]
#    ^Operation  ^Schleife    ^Filterbedingung
 
female_names = [name for name in names if is_female(name) ]
 
Aufgabe 5:
Gegeben ein Array, das reelle Zahlen enthält, erzeuge ein Array, das die Dezimalrepräsentation als Strings enthält.
[1.2, 2.55] =>  [ '1.2', '2.55' ]
Lösung:
a = [1.2, 2.55]
b = [str(k) for k in a]
 
Aufgabe 6:
Verwenden Sie fortgeschrittene Formatierungsanweisungen für die Transformation von floats in Strings. Genauer: Schreiben Sie eine Funktion, die die Float-Zahlen 
  • auf 2 Nachkommastellen gerundet in einen String umwandelt, 
  • die Tausender-Stellen durch Apostroph ' gruppiert, und
  • am Anfang so viele Leerzeichen einfügt, dass der String die Länge 16 hat.
Wenden Sie diese Funktion mittels map oder 'list conprehension' auf ein Array von Floats an.
Beispiel:    12345.6789  => "       12'345.68"
 
Lösung:
def myformatting(x):
        string = "{:16,.2f}".form    at(x)
        return string.replace(",","'")
a = [15424.26546, 2458.55465]
b = [myformatting(k) for k in a]
 
Hinweis 1: um die Tausender für das Apostroph abzuzählen, bietet sich der Indexoperator mit negativen Indizes an, weil man ja vom Dezimalpunkt rückwärts zählen muss.
 
Hinweis 2: Einen String der Länge 16 mit Leerzeichen aufgefüllt bekommt man sehr einfach, indem man
result = ("                " + s)[-16:]
schreibt (überlegen Sie selbst, warum dies funktioniert). Python-Strings implementieren dies in der vordefinierten Funktion rjust():
result = s.rjust(16)      # mit Leerzeichen füllen
result = s.rjust(16, '0') # mit Nullen füllen
 
Hinweis 3: wichtige Formtierungsanweisungen für Strings mit Platzhaltern, die mit der format()-Funktion ersetzt werden sollen: 
  • Platzhaltersymbol sind die geschweiften Klammern. Die Argumente der format()-Funktion werden den Platzhaltern in der selben Reihenfolge zugeordnet:
"Der {}. {}. {} ist Mittwoch".format(6,"Apr",2016
=> "Der 6. Apr. 2016 ist Mittwoch"
  • Ein andere Reihenfolge kann man erzwingen, wenn man den Index des gewünschten Arguments in die Klammern schreibt:
"Der {2}/{1}/{0} ist Mittwoch".format(6,"Apr",2016)
=>  "Der 2016/Apr/6 ist Mittwoch"
  • Die Formatierung des jeweiligen Arguments wird hinter einem : angegeben. Am wichtigsten sind die Typangaben: d für Dezimalzahl, f für fixed point "12.345" und e für wissenschaftliche Schreibweise "1.2345e1".
"{:d}  {:f}   {:e}".format(14, 12.345, 23.123)
"14  12.345000   2.312300e+01"
  • eine Zahl nach dem Doppelpunkt gibt die Mindestlänge der Ausgabe an - wenn die auszugebende Zahl kürzer ist, wird links mit Leerzeichen aufgefüllt. Zahlen, die zu lang sind, werden immer vollständig ausgegeben (Korrektheit geht vor Formatierung). Steht vor der Länge eine 0, wird mit Nullen statt Leerzeichen aufgefüllt.
"{:8d}".format(10)   # =>  "      10"
"{:08d}".format(10)  # =>  "00000010"
  • bei float (f und e): .xf bzw. .xe gibt die Zahl der Nachkommastellen an, wobei fürx eine Zahl eingesetzt werden muss.
"{:.2f}".format(1.2383)  # =>  "1.24"
"{:.3f}".format(1.2383)  # =>  "1.238"
"{:8.2f}".format(1.2383) # =>  "    1.24"
  • weitere Formatierungen existieren, um die Ausrichtung der Vorzeichen, die Gruppierung der Tausender, die Ausgabe als Hexadezimalzahl usw. zu steuern. Da dies nur selten benötigt wird, sollte man jeweils in der Dokumentation nachschauen, wie eine solche Spezialformatierung funktioniert.
 
Eine gute Erklärung der Formatsyntax findet sich in http://www.python-course.eu/python3_formatted_output.php
(dort wird sowohl die alte Syntax %.2f etc. mit String-Modulo wie auch die neue {:.2f} mit string.format() erklärt.
 
Übungsblatt 3:
  • Wörter zerwürfeln
  • Text entschlüsseln mittels Häufigkeitsanalyse
 

Lektion 4

 Der Typ dict ("Dictionary")
 
Arrays ordnen Indizes, die von 0 bis len(array)-1 reichen, jeweils ein Arrayelement zu. Der Zugriff über zusammenhängende Integer-Indizes kann sehr effizient implementiert werden, aber sie sind in vielen Anwendungen unpraktisch. Der Typ dict funktioniert ähnlich wie ein Array, aber die Indizes oder, allgemeiner, die Schlüssel sind beliebig:
  • beliebige ganze Zahlen (nicht zwischen 0 und len(array)-1)
d = {}      # geschweifte statt eckige Klammern
d = dict()  # Konstruktoraufruf
d[1000] = 1
d[-10] = 2
  • Strings
d = {}
d['erstes'] = 1
d['zweites'] = 2
 
  • Alternative Eingabe: 
a = {'eins': 1,'fünf': 5, 3: 3}
  • beliebige Objekte
 
Z.B.: Zählen, wie oft ein Wort in einem Text (langer String) vorkommt (wir vernachlässigen hier die Satzzeichen):
text = ...
text = text.lower()
d = {}
for word in text.split(' '):
    if word in d:     # ist der Index 'word' vorhanden?
        d[word] += 1  # ja => Hochzählen
    else:
        d[word] = 1   # nein => Initialisieren
 
Nützliche Funktionen (mit dir(dict) abrufbar, Terminologie: 'item' bezeichnet in Python-Dictionaries ein (key, value)-Paar):
list(d.keys())    # gibt eine Liste mit den  
                  # Schlüsseln aus
list(d.values())  # gibt eine Liste mit den Werten 
                  # aus
list(d.items()) # gibt eine Liste mit den Schlüsseln
                # und Werten aus
# alle Schlüssel und Werte ausgeben:
for key, value in d.items():
    print('key: ', key, ' holds value ', value)
d.fromkeys(d2, xval) # iteriert über d2 und gibt ein
                     # neues Dictionary mit den
                     # übereinstimmenden Schlüsseln
                     # und dem dafür neu 
                     # festgelegten Wert xval
                     # (muss nicht angegeben
                     # werden, dann:None) aus
d.pop(xkey) # löscht den Schlüssel xkey und den
            # zugehörigen Wert aus dem Dictionary und gibt
            # den Wert zurück
d.popitem() # löscht irgendein item aus dem
            # Dictionary und gibt es aus
d.update(d2) # überschreibt das Dictionary d mit den
             # Werten des Dictionarys d2
             # Elemente, deren Schlüssel in d, 
             # aber nicht in d2 vorkommen, 
             # bleiben unverändert
In Python ist fast alles ein Dictionary:
 
Der Typ SET
Ein Set verhält sich wie eine mathematische Menge
Beispiel:
a = set() # erzeugt leeres Set
b = {1,2,3}
Arrays in Set umwandeln (Mehrfacheinträge werden nur einmal übernommen):
c= set([1, 2, 3, 4, 3, ])
Wichtige Funktionen (mit dir(set) abrufbar):
c.discard(1) # löscht das Element 1 aus dem Set,
             # tut nichts, wenn 1 nicht enthalten ist
c..discard(1) # löscht das Element 1 aus dem Set,
              # löst 'KeyError' aus, wenn 1 nicht enthalten ist
c.add(5) # fügt das Element 5 zum Set hinzu
c.difference
c.subset
c.intersect
 
Aufgabe 1:
Lesen Sie einen String ein und stellen Sie fest, wie viele verschiedene Zeichen er enthält. Variante: das selbe, aber Groß-/Kleinschreibung wird ignoriert.
 
Lösung:
print(len(set(mystring)))
 
Sortieren:
Es gibt zwei wichtige Sortierfunktionen:
  • als Funktion des Array-Typs: array.sort() 
  • als freie Funktion für alle 'iterables': sorted(iterable) 
 
Aufgabe 2:
  1. Array mit Zahlen erzeugen und sortieren, einmal in-place und einmal mit Erzeugung eines neuen Arrays
  1. absteigend sortieren, mit Parameter reverse=True 
  1. Die Items eines Dictionary sortieren, einmal nach den Schlüsseln, einmal nach den Werten.
  • Hinweis: Dazu hat die sorted()-Funktion einen Parameter key, der angibt, wonach sortiert werden soll. Am einfachsten übergibt man hier eine lambda-Funktion, die den gewünschten Sortierschlüssel extrahiert:
  # a enthält Tupel der Länge 2
  sorted(a, key=lambda x: x[0]) 
  # sortiere nach Element 0 ^ der Tupel
  • Weil man dies häufig benötigt, hat Python bereits eine Convenience-Funktion
  # abgekürzte Schreibweise für obiges
  import operator
  sorted(a, key=operator.itemgetter(0))
  # noch mehr abgekürzt
  from operator import itemgetter
  sorted(a, key=itemgetter(0))
  • Lösung:
  d = {1: 'eins', 2: 'zwei', 3: 'drei' }
  sorted(d.items(), key=itemgetter(0))  
  1. Den Schlüssel-Parameter kann man auch benutzen, um die zu sortierenden Werte vor dem Vergleichen umzurechen, indem man eine Umrechnungsfunktion an key übergibt. 
  1. Gegeben ein Array mit positiven und negativen Zahlen, impementieren Sie eine Sortierung nach Absolutbetrag.
    a = [2, -3, 5, -1, 6, 7, -5]
    a.sort(key=abs)  # in-place
  1. Sortieren Sie ein Array von Strings nach der String-Länge.
    b = ['asdasd', 'asd', '', '1234']
    c = list(sorted(b, key=len)) # neues Array
  1. Verrückte Sortier-Reihenfolgen kann man mit einer Vergleichsfunktion erzwingen. Eine Vergleichsfunktion hat zwei Argumente (=Schlüssel, die verglichen werden sollen) und einen dreiwertigen Return:
  compare(a, b) # =>   -1  <=> a < b
                # =>    0  <=> a == b
                # =>    1  <=> a > b
  • (wählen Sie den Namen Ihrer Vergleichsfunktion so, dass er etwas über die Sortierung aussagt, die realisiert werden soll) Man muss die Compare-Funktion mit der Hilsfunktion cmp_to_key in einen Sortierschlüssel umwandeln:
   from functools import cmp_to_key
   sorted(array, key=cmp_to_key(compare))
  1. Implementieren Sie die absteigende Sortierung aus 2 mit Hilfe einer Compare-Funktion (der Parameter reverse darf nicht verwendet werden).
  1. Gegeben ein Array mit positiven und negativen sowie geraden und ungeraden Zahlen: sortieren Sie so, dass gerade Zahlen nach dem Absolutbetrag, ungerade nach ihrem Wert sortiert werden.
  1. Gegeben ein Array mit Strings "Vorname Nachname", sortieren Sie alphabetisch nach Nachnamen, und bei gleichen Nachnamen nach der Länge des Vornamens.
    def name_compare(s1, s2):
        v1, n1 = s1.split(' ') # Vor- und Nachname
        v2, n2 = s2.split(' ')
        if n1 == n2:
            l1, l2 = len(v1), len(v2)
            if l1 < l2:
                return -1
            elif l1 == l2:
                return 0
            else:
                return 1
        elif n1 < n2:
            return -1
        else:
            return 1
    
    names = ["Hanno Müller", ...]
    sorted(names, key=cmp_to_key(name_compare))
    
Exkurs 'selection sort':
    def selection_sort(array): # sortiere in-place
        l = len(array)
        for i in range(0,l-1):
            # suche kleinstes Element des Rest-Arrays [i:l]
            smallest = i
            for k in range (i+1,l):
                # die if-Bedingung bestimmt das Sortier-Ergebnis
                # * default (wie jetzt): aufsteigende Sortierung
                # * array[smallest]<array[k]: absteigende Sortierung
                # * key(array[k])<key(array[smallest]): Sortierung nach key
                # * compare(array[k], array[smallest])<0: mit Compare-Funktion
                if array[k]<array[smallest]: 
                    smallest=k
            # bringe kleinstes Element des Rest-Arrays [i:l] an Position i
            array[i],array[smallest] = array[smallest],array[i]
      
Die Funktion sorted() benutzt zwar einen effizienteren Sortieralgorithmus, aber die Vergleiche sind die selben, und werden je nach den verwendeten Funktionsparametern wie oben ersetzt.       
 
Iteratoren:
 
Viele Datenstrukturen sind "Container", d.h. Behälter für andere Daten, z.B. list, tuple, dict (für beliebige Elementtypen), str (für Zeichen) usw. Oft hat man die Aufgabe, in einer Schleife mit allen (oder ausgewählten) Elementen des Containers etwas zu tun. Eine naive Implementierung erfordert dann, dass man für jeden Containertype die Schleife anpassen muss. 
Z.B. sorted(): man braucht eine Version für list, eine für dict, eine für str usw. => Explosion von Code, der aber eigentlich immer dasselbe tut. Das macht nicht nur mehr Arbeit, sondern ist auch schwer zu debuggen, weil jedes Bug in allen Versionen korrigiert werden muss.
 
Iteratoren abstrahieren die Aufgabe, über die Elemente eines beliebigen Containers zu iterieren, in einer Vorgehensweise, die vom Container unabhängig ist. => Eine Funktion für Iteratoren muss nur einmal implementiert werden und funktioniert dann für alle Container, die Iteratoren unterstützen.
 
a = {'erstes': 1, 'zweites': 2, 42: 'zweiundvierzig' }
 
# erzeuge Iterator für keys
i = a.keys().__iter__() 
 
# die kanonische Schleife mit Iterator
try:
    while True: # Endlosschleife 
                # wird durch Exception beendet
        element = next(i) # liefert nächstes Element
                          # oder raise Stopiteration
                          # wenn kein Element mehr
                          # vorhanden
        print(element) # tue etwas mit element
except StopIteration: # fange die erwartete 
                      # Exception auf
    pass
 
# Output
zweites
42
erstes
 
# Kurzschreibweise, Python generiert obiges daraus
for element in a.keys():
    print(element)
    
# Output
zweites
42
erstes
 
Nützliche Funktionen aus itertools:
for i in zip(a,b): # erzeugt Paare aus den 
                   # Arrays a und b. Die Anzahl der
                   # Iterationen entspricht dem 
                   # kürzesten Array
    print(i)
    
for i in itertools.zip_longest(a,b): # wie zip, 
                   # aber die Anzahl der
                   # Iterationen entspricht dem 
                   # längsten Array.
                   # Fehlende Werte werden mit 
                   # 'None' aufgefüllt. Mit dem
                   # Parameter 'fillvalue' kann
                   # man einen anderen Füllwert 
                   # wählen
    print(i)
    
for i in itertools.product(a,b): # erzeugt alle aus a
                                 # und b möglichen
                                 # Paare
    print(i)
 
Aufgabe 3:
Gegeben sind zwei Arrays mit den Werten ["sieben", "acht", ... "Ass"]und den Farben ["Karo ", ...]eines Kartenspiels. Verwenden Sie product um das ganze Kartenspiel zu erzeugen.
 
Lösung:
karten = list()
for i in itertools.product(werte,farben):
        karten.append(i)
 
Funktion reduce aus dem Modul functools, Beispiel:
summe = functools.reduce (lambda x, y: x + y, a, 0
            # erster Parameter: function, zweiter 
            # Parameter: sequence,  
            # optionaler dritter Parameter: Startwert
 
Aufgabe 4:
Die Kartenwerte aus Aufgabe 3 sind jetzt als dict gegeben, wobei die keys die Kartenwerte wie zuvor sind, und die values die Werte der Karten beim Skat
werte = { 'sieben': 0, 'zehn': 10, ... }
  1. Erzeugen Sie jetzt das Kartenspiel so, dass ein Dictionary mit den Skat-Werten herauskommt (die Schlüssel sind die Namen der Karten):
skat = { "Pik sieben": 0, "Kreuz Ass": 11, ...}
  1. Benutzen Sie shuffle aus dem Module random, um das Kartenspiel zu mischen, genauer: eine Liste der Items des Dictionary skat. Beachten Sie, dass random.shuffle() in-place arbeitet.
Nehmen Sie die ersten zehn Karten des gemischten Spiels als Ihr Blatt auf und benutzen Sie reduce, um den Wert Ihres Blattes auszurechnen.
 
Lösung:
import itertools
 
werte = ['sieben', 'acht', 'neun', 'zehn', 'Bube', 'Dame', 'Koenig', 'Ass']
farben = ['Karo', 'Pik', 'Kreuz', 'Herz']
 
werteDict = { 'sieben': 0, 'acht': 0 , 'neun': 0, 'zehn': 10, 'Bube': 2, 'Dame': 3, 'Koenig': 4, 'Ass': 11}
 
skat = {}
for i in itertools.product(farben, werte):
        skat[i[0]+' ' +i[1]] = werteDict[i[1]]
 
Übungsblatt 4:
  • Versionsnummern sortieren
  • Löffelsprache
 

Lektion 5

Eigene Typen:
 
Dafür gibt es in Python das Schlüsselwort class:
class MyClass(Basisklasse1, Basisklasse2, ...):
    ... # Methoden (Funktionen) der Klasse
 
Die grundlegende Basisklasse heisst object. Man verwendet sie, wenn man keine anderen Basisklassen benötigt.
class MyClass(object):
    ...
 
Alle Methoden haben als ersten Parameter self, der auf den Speicher der Klasse selbst verweist. self verhält sich programmiertechnisch wie eine ganz normale Variable.
 
Die wichtigste Methode ist der Konstruktor. Dafür verwendet Python den Funktionsnamen __init__:
class MyClass(object):
    def __init__(self, arg1, arg2, ...):
        ... # Initialisierung von 'self'
 
Dann kann ein neues Objekt dieses Types durch den Konstruktoraufruf erzeugt werden, genau wie bei list oder dict:
my_object = MyClass(arg1, arg2, ...)
 
Aufgabe 1:
Schreiben Sie eine Klasse Point für einen zweidimensionalen Punkt, der als Konstruktorargumente die Koordinaten x und y übergeben und dann in self.x  und self.ygespeichert werden. (Die Notation self.x definiert eine Variable, die zum aktuellen Objekt gehört.)
 
Lösung:
class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
point = Point(10, 20)
 
Oft ist es nützlich, dass Klassen einen 'Default-Konstruktor' besitzen, der keine Argumente hat und das Objekt in einen Standrd-Initialzustand versetzt. Bei int und float ist das z.B. die Zahl 0. Dies erreicht man zweckmäßig. indem man den Konstruktorargumenten Defaultwerte zuweist:
class Point(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        
point = Point()
assert point.get_x() == 0 and point.get_y() == 0
point1 = Point(1) # => Point(1,0)
 
Zum Abfragen der Koordinaten verwendet man 'getter'-Methoden:
class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def get_x(self):
        return self.x
    def get_y(self):
        return self.y
        
point = Point(10, 20)
print(point.get_x(), point.get_y())
 
Entsprechend verwendet man setter-Methoden, um die Koordinaten zu verändern:
 
    ...
    def set_x(self, x_new):
        self.x = x_new
    def set_y(self, y_new):
        self.y = y_new
        
point = Point(10, 20)
point.set_y(-10)
# Python macht daraus intern den Aufruf
# Point.set_y(point, y)
#             ^ self
 
Um einen Point mit print ausdrucken zu können, muss man ihn in einen String umwandeln können. Dazu muss man die Methode __str__ implementieren:
    ...
    def __str__(self):
        return "({}, {})".format(self.x, self.y)
   
Nützlich ist außerdem, zwei Punkte auf Gleichheit und Ungleichheit testen zu können. Dafür verwendet man die Methoden __eq__ und __ne__.
 
Aufgabe 2:
Implementieren Sie diese Methoden und testen Sie sie an einigen Beispielen mit assert.
 
Lösung:
class Point(object):
    ..... # all other methods of the class
    def __eq__(self, other):
            return self.x == other.x and self.y == other.y
    def __ne__(self, other):
            return self.x != other.x and self.y != other.y
p1 = Point(100,100)
p2 = Point(100,100)
p3 = Point(12,100)
# tests
assert p1 == p2
assert p1 != p3
 
Wenn man außerdem die Vergleichoperatoren < usw. implementiert, sollte gelten
'not p1 < p2 and not p2 < p1'  <=> 'p1 == p2'
 
Bei Punkten kann man entweder ganz auf die Vergleichsoperatoren verzichten (wie bei komplexen Zahlen). Wenn man diese Operatoren dennoch benötigt, z.B. um eine sortierte Punktliste zu erzeugen oder um Punkte in einen sortierten Suchbaum einfügen zu können, kann man eine willkürlich Sortierung wählen. Üblich ist die 'Scan-Order-Sortierung', d.h. die y-Koordinate hat Vorrang:
    ...
    def __lt__(self, other):
        if self.y == other.y:
            return self.x < other.x
        else:
            return self.y < other.y
 
Aufgabe 3:
Implementieren Sie die anderen Vergleiche <=, >,  >= nur unter Verwendung des <-Operators und not und testen Sie alle Vergleiche mittels assert.
 
Lösung:
class Point(object):
    ....
    def __lt__(self, other):
        if self.y == other.y:
            return self.x < other.x
        else:
            return self.y < other.y
    def __ge__(self,other):
        return not self < other
    def __gt__(self,other):
        return not self < other and not self == other
    def __le__(self,other):
        return not self > other
 
p1 = Point(100,100)
p2 = Point(100,100)
p3 = Point(12,100)
 
assert p3 < p1
assert p3 >= p3
assert p1 > p3
assert p1 <= p1
assert p3 <= p1 # ... usw.
 
Ein Point-Objekt kann als Array der Länge 2 interpretiert werden, wobei point[0] die x-Koordinate und point[1] die y-Koordinate repräsentieren. Um den Index-Operator zu unterstützen, muss man die Methoden __getitem__ (lesender Zugriff) und __setitem__ (schreibender Zugriff) implementieren:
    ...
    def __len__(self):
        return 2
    def __getitem__(self, index):
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        else:
            raise IndexError("Point[index]: index must be 0 or 1")
            
    def __setitem__(self, index, value):
        if index == 0:
            self.x = value
        elif index == 1:
            self.y = value
        else:
            raise IndexError("Point[index]: index must be 0 or 1")
 
Da jetzt __len__, __getitem__ und __setitem__ vorhanden sind, kann Python drei Arten von Schleifen unterstützen:
point = Point(10, 20)
 
# automatische Schleife
for coord in point:
    print(coord)
 
# Schleife mit expliziter Indexierung
for k in range(len(point)):
    coord = point[k]
    print(coord)
 
# Schleife mit IndexError als Stop-Kriterium
try:
    k = 0
    while True:
        coord = point[k]
        print(coord)
        k += 1
except IndexError:
    pass
 
# Ausgabe in allen Fällen
10   # x-Koordinate
20   # y-Koordinate
 
Aufgabe 4:
Implementieren Sie Addition und Subtraktion von zwei Punkten, sowie Multiplikation und Division von einem Punkt mit einem Skalar, und die Funktionen abs, round und die Negation -point jeweils so, dass ein neues Objekt erzeugt wird (also nicht in-place). Testen Sie die Funktionen mit assert. Informieren Sie sich auf der Seite https://docs.python.org/3/reference/datamodel.html#special-method-names über die zu implementierenden Methoden.
 
Wenn Sie die __mul__-Methode implementiert haben, funktioniert jetzt point *2, aber 2 * point funktioniert nicht, obwohl die Multiplikation kommutativ sein sollte. Dafür gibt es die __rmul__-Methode. Die r-Varianten werden von Python aufgerufen, wenn in einem arithmetischen Ausdruck das Objekt rechts vom Operatorzeichen ist. Implementieren und testen Sie die __rmul__-Methode!
 
Außerdem ist es wünschenswert, dass die arithmetischen Operationen zusätzlich in-place ausgeführt werden können, z.B. point *= 2. Dazu dienen die Methoden __imul__usw.  Implementieren und testen Sie auch diese Methoden.
 
Die Bild-Klasse Image:
 
Die Klasse Image speichert ein zweidimensionales Bild, zunächst nur als Graubild, evtl. behandeln wir Farbbilder später. Die Klasse benutzt ein Python-Array als Speicher für die Pixel:
class Image(object):
    def __init__(self, width=0, height=0):
        # vermeide Redundanz => Größe festlegen
        # und Speicher anlegen nur einmal
        # implementieren und hier wiederverwenden
        self.resize(width, height)
        
    def get_width(self):
        return self.width
        
    def get_height(self):
        return self.height
        
    def resize(self, new_width, new_height):
        self.width = new_width
        self.height = new_height
        # alle Pixel mit 0 initialisieren
        self.pixel = [0]*(new_width*new_height)
        
 
Um auf die Pixel zuzugreifen, ist die Index-Notation am bequemsten:
image = Image(100, 200) # Konstruktor
 
image[10, 12] = 100 # Pixel (10, 12) setzen
print(image[0,0])   # Pixel (0, 0) lesen
 
Dazu implementiert man wiederum die Methoden __getitem__ und __setitem__, aber diesmal mit einem tuple-Argument, das die beiden Koordinaten x und y enthält (plus self).
 
Aufgabe 5:
Implementieren und testen Sie diese Funktionen. Überlegen Sie dabei vor allem, wie man 2-dimensionale Koordinaten (x, y) in 1-dimensionale Indizes für das Array self.pixels umrechnet.
 
Hinweis: Wenn der Indexoperator mehr als ein Argument hat, z.B. image[0, 0], werden alle Indizes in ein Tuple eingepackt. Das heisst, x ist index[0] und y ist index[1].
 
Lösung:
    ...
    def __getitem__(self, coord):
        index = coord[0] + self.width*coord[1]
        return self.pixel[index]
 
    def __setitem__(self, coord, value):
        index = coord[0] + self.width*coord[1]
        self.pixel[index] = value
 
Da die Klasse Point  die selben Indexfunktionen unterstützt wie tuple, kann man die Funktionen __getitem__ und __setitem__ auch mit Point aufrufen:
assert image[3,4] == image[Point(3,4)]
x, y = 3, 4
point = Point(x,y)
assert image[x,y] == image[point]
 
Um ein Bild anzuzeigen, ist es am einfachsten, eine Umwandlungsfunktion __str__ nach String zu schreiben. Der String soll jedes Pixel als dreistellige Zahl ausgeben, jeweils mit Leerzeichen getrennt. Am Ende jeder Zeile soll ein Zeilenumbruch eingefügt werden. Damit Pixel mit gleicher x-Koordinate übereinander stehen, sollen Pixelwerte kleiner 100 links mit Leerzeichen aufgefüllt werden (dabei hilft str.format()!):
"  0 123  12\n
  42   5 255\n
 156  27   9"
 
Aufgabe 6:
Implementieren Sie die __str__-Funktion. Füllen Sie das Bild mit einem Schachbrettmuster mit den Werten 0 und 255 und geben Sie das Ergebnis mit print aus. Beispiel für ein (3x3)-Bild:
  0 255   0
255   0 255
  0 255   0
 
Wir wollen das Bild natürlich auch auf Festplatte speichern und dann mit einem normalen Bildbetrachter ansehen. Dafür biete sich das PGM-Fileformat an, weil es mit Abstand am einfachsten zu implementieren ist. Zum Öffnen von PGM-Bildern ist unter Windows 'IrfanView'  und unter Linux/Mac das Programm display aus ImageMagick geeignet.
 
Aufgabe 7:
Schreiben Sie eine Funktion writePGM(image, filename), die das Bild als PGM-File speichert, siehe https://de.wikipedia.org/wiki/Portable_Anymap#Graymap . Zusammengefasst: Das PGM-Format beginnt immer mit dem String P2 auf einer eigenen Zeile, dann folgen <width> <height> als Dezimalzahlen auf einer eigenen Zeile, danach <max_value> (oder einfach 255) als Dezimalzahl auf einer eigenen Zeile, und schließlich die Pixelwerte wie von str(image) ausgegeben.
 
Lösung:
def writePGM(image, filename):
    with open(filename, 'w') as file:
        file.write('P2\n')
        file.write('{} {}\n'.format(image.get_width(), image.get_height()))
        file.write('255\n')
        file.write(str(image))
 
Aufgabe 8:
Erzeugen Sie nun ein größeres Schachbrett als zuvor (z.B. 200x200 Pixel) mit größen Feldern (z.B. 20x20 Pixel) und schauen Sie das Ergebnis im Viewer Ihrer Wahl an.
 
Lösung:
width, height = 200, 200
feld = 20
image = Image(width, height)
for y in range(height):
    for x in range(width):
        if (x // feld + y // feld) % 2 == 0:
            image[x,y] = 255
        else:
            image[x,y] = 0
writePGM(image, 'schachbrett.pgm')
 
Übungsblatt 5:
  • class Rectangle 
  • Kaleidoskop
 

Lektion 6

Aufgaben zur Bildbearbeitung
 
Aufgabe 1:
Implementieren Sie einen Algorithmus smoothImage(image) => smoothed_image, der ein Bild unscharf macht , indem er für jedes Zielpixel den Durchschnitt in einer 3x3-Umgebung um das Quellpixel einsetzt. Exportieren Sie das Ergebnis mit writePGM() und vergleichen Sie in einem Bildbetrachter mit dem Original. Wiederholen Sie das Experiment so, dass smoothImage() iterativ angewendet (d.h. das aktuelle Ergebnisbild noch mehr geglättet) wird. Randpixel (wo das 3x3-Fenster teilweise außerhalb des Bildes liegt) sollen in der ersten Fassung des Algorithmus übersprungen werden. Beachten Sie auch, dass das PGM-Format nur ganze Zahlen speichern kann.
 
Lösung:
def smoothImage(image):
    result = Image(image.width, image.height)
    for y in range(1, image.height-1):
        for x in range(1, image.width-1):
        #              ^ Rand skippen ^
            sum = 0
            for yy in range(y-1, y+2):
                for xx in range(x-1, x+2):
                    sum += image[x,y] 
            average = round(sum / 9)
            result[x, y] = average
 
Aufgabe 2:
Implementieren Sie in der Image-Klasse eine Methode get_reflected(x,y), die auf für Pixel außerhalb des Bildbereichs funktioniert und dort Werte entsprechend der reflektiven Randbedingungen zurückgibt. Beispiel:
assert image.get_reflected(1,2) == image[1,2]
assert image.get_reflected(-1,-2) == image[1,2]
Negative Indizes werden dabei mit ihrem Absolutbetrag verwendet. Leiten Sie die Umrechnung für Indizes >= width bzw. >= height selbst her. Testen Sie die Korrektheit Ihrer Methode. Achten Sie dabei besonders auf "off-by-one errors", d.h. die umgerechnete Index liegt um eins daneben (das sieht man in der Ausgabe möglicherweise gar nicht sofort).
 
Lösung:
class Image():
    ...
    def get_reflected(self, x, y):
        if x < 0:
            x = -x
        elif x >= self.width:
            x = 2*self.width-2-x
        if y < 0:
            y = -y
        elif y >= self.height:
            y = 2*self.height-2-y
        # jetzt enthalten x und y gültige Indizes, 
        # eventuell sind diese reflektiert
        return self[x,y]
Test-driven development: Wir nehmen ein 3x3-Bild, füllen das mit Werten, so dass Fehler in get_reflected leicht erkennt, und implementieren einige Tests (die natürlich anfangs fehlschlagen, weil get_reflected noch nicht fertig ist):
image = Image(3,3)
# fülle mit aufsteigenden Zahlen
image.pixel = range(9)
print(image)
# Ausgabe
#   0   1   2
#   3   4   5
#   6   7   8
 
assert image.get_reflected(-1, 0) == 1
assert image.get_reflected(-2, 0) == 2
 
assert image.get_reflected(0, -1) == 3
assert image.get_reflected(1, -2) == 7
 
assert image.get_reflected(3, 0) == 1
assert image.get_reflected(4, 0) == 0
 
assert image.get_reflected(0, 3) == 3
assert image.get_reflected(1, 4) == 1
 
Aufgabe 3:
Modifizieren Sie  smoothImage(image) aus Aufgabe 1 so, dass auf die Pixel mit get_reflected() zugegriffen wird und die Randpixel nicht mehr übersprungen werden müssen.
 
Aufgabe 4:
Implementieren Sie eine Funktion drawLine(image, p0, p1, value), die den Bresenham-Algorithmus (https://de.wikipedia.org/wiki/Bresenham-Algorithmus) verwendet, um in image eine Linie von p0 nach p1 mit dem Grauwert value zu zeichnen. Beachten Sie dabei das Clipping, falls p0 oder p1 (oder beide) sich außerhalb des Bildes befinden.
 
Lösung:
def drawLine(image, p0, p1, value):
    dx = p1[0] - p0[0]
    dy = p1[1] - p0[1]
    steps = max(abs(dx), abs(dy))
    dx /= steps
    dy /= steps
    # range(step+1), weil sonst der Punkt p1 nicht 
    # gezeichnet wird (Zählung ab 0)
    for k in range(steps+1):
        # Geradengleichung mit Parameter k
        x = p0[0] + round(k*dx)
        y = p0[1] + round(k*dy)
        # clipping condition
        if x >= 0 and x < image.width and \
           y >= 0 and y < image.height:
        # erlaubt ist in Python auch:
        # if 0 <= x < image.width and ...
            image[x, y] = value
 
Aufgabe 5:
Implementieren Sie eine Funktion drawRectangle(image, p0, p1, value), die drawLine() benutzt, um den Rand des Rechtecks mit den gegenüberliegenden Ecken p0 und p1 zu zeichnen.
 
Lösung:
def drawRectangle(image, p0, p1, value):
    p2 = Point(p0[0], p1[1])
    p3 = Point(p1[0], p0[1])
    # Äquivalent und vielleicht lesbarer wäre
    # p2 = Point(p0.x, p1.y)
    # p3 = Point(p1.x, p0.y)
    drawLine(image, p0, p2, value)
    drawLine(image, p2, p1, value)
    drawLine(image, p1, p3, value)
    drawLine(image, p3, p0, value)    
 
Übungsblatt 6:
  • Conway's Game of Life
  • Bonus: daraus ein Video erstellen
 

Lektion 7

Klassen zum Zeichnen
 
Wir wollen jetzt die Zeichenfunktionen in Klassen einbetten, damit man eine Szene aufbauen kann, die sich selbst zeichnet.
 
Jede Zeichenklasse speichert intern die Parameter, die zum Zeichnen des jeweiligen Objekts nötig sind, also z.B. die Endpunkte für eine Linie, die Ecken für ein Rechteck, Mittelpunkt und Radius für einen Kreis.
 
class Line:
    def __init__(self, p0, p1, value):
        self.p0 = p0
        self.p1 = p1
        self.value = value
        
    def draw(self, image):
        drawLine(image, 
                 self.p0, self.p1, self.value)
 
Aufgabe 1:
Erweitern Sie class Rectangle aus der Hausaufgabe entsprechend und testen Sie beide Klassen. Definieren Sie im Rectangle-Konstruktor einen Defaultwert für die Zeichenfarbe, damit das erweiterte Rectangle mit existierendem Code (der den value-Parameter noch nicht kennt) kompatibel bleibt.
 
Wir definieren jetzt ein Klasse Drawing, die als Container für andere Zeichenobjekte dient.
class Drawing:
    def __init__(self):
        self.items = []
        
    def add(self, item):
        self.items.append(item)
        
    def draw(self, image):
        for item in self.items:
            item.draw(image)
 
Aufgabe 2:
Implementieren Sie Drawing, fügen Sie einige Zeichenobjekte ein und testen Sie, ob alles funktioniert. Fügen Sie zum Beispiel vier Line-Objekte ein, die zusammen eine Raute ergeben.
 
Email Niels: buwen AT stud.uni-heidelberg.de falls es Probleme mit der Bewertung der Übungsaufgaben gibt.
 
Man kann ein Drawing auch in ein anderes Drawing einbetten:
rect = Rectangle(Point(9,11), Point(21,19), 255)
line = Line(Point(9,19), Point(21,11), 128)
draw = Drawing()
draw.add(rect)
draw.add(line)
 
outer = Drawing()
outer.add(draw)
outer.add(draw)
 
image = Image(150,100)
outer.draw(image)
 
Wenn Sie das ausprobieren, werden Sie feststellen, dass die beiden Kopien von draw genau übereinander liegen, so dass das Kopieren zunächst keinen sichtbaren Effekt hat. 
 
Wir wollen, dass eine Kopie gegenüber der anderen verschoben wird. Um diese Möglichkeit vorzubereiten, fügen wir in die Zeichenklassen Line, Rectangle und Drawing eine neue Zeichenfunktion drawShifted(self, image, offset) ein, die das Objekt um einen offset verschoben zeichnet.
 
Aufgabe 3:
Fügen Sie drawShifted() in die Zeichenobjekte ein. Die Linie wird dann z.B. von p0+offset nach p1+offset gezeichnet, statt von p0 nach p1. Testen Sie, dass ein Aufruf von drawShifted() die Zeichnung tatsächlich um den erwarteten Offset veschoben zeichnet.
 
Jetzt implementieren wir ein neues Zeichenobjekt Shifter, das sich einen Offset merkt und dann ein eingebettetes Zeichenobjekt entsprechend verschoben zeichnet.
class Shifter:
    def __init__(self, item, offset):
        # 'item' ist das Zeichenobjekt, das 
        # verschoben gezeichnet werden soll
        self.item = item 
        self.offset = offset
        
    def draw(self, image):
        self.item.drawShifted(image, self.offset)
       
    def drawShifted(self, image, offset):
        total_offset = self.offset + offset
        self.item.drawShifted(image, total_offset)
 
Wiederholen Sie jetzt das Beispiel von oben, aber fügen Sie die Kopie der Zeichnung verschoben ein:
rect = Rectangle(Point(9,11), Point(21,19), 255)
line = Line(Point(9,19), Point(21,11), 128)
draw = Drawing()
draw.add(rect)
draw.add(line)
 
outer = Drawing()
outer.add(draw)
# füge Kopie verschoben ein
outer.add(Shifter(draw, Point(10,10)))
 
image = Image(150,100)
outer.draw(image)
 
Aufgabe 4:
dieses Beispiel mit Ihrer eigenen Zeichnung (z.B. mit der Raute) nachvollziehen.
 
Aufgabe 5:
  • Implementiere eine Zeichenklasse EmbeddedImage, die ein Image speichert und es beim Aufruf von drawab der Koordinate (0, 0) in das gegebene Bild einzeichnet, bzw. ab Koordinate offset, wenn drawShifted aufgerufen wird:
class EmbeddedImage:
    def __init__(self, image):
        self.image = image
        
    def draw(self, target_image):
        # verwende hier 'drawShifted()' mit offset=(0,0)
        # um Codeduplizierung zu vermeiden
        self.drawShifted(target_image, Point())
        
    def drawShifted(self, target_image, offset):
        ... # your code here
 
Beispiel für die Verwendung (das ist gleichzeitig Ihr Test):
# ändern Sie den folgenden Aufruf so, wie Ihre
# Bildklasse aus einem File aufgebaut wird 
image = readPGM("filename")
 
embed = EmbeddedImage(image)
drawing = Drawing()
drawing.add(embed)
# füge eine verschobene Kopie von 'embed' ein
drawing.add(Shifter(embed, Point(100,100)))
 
target_image = Image(350,300)
drawing.draw(target_image)
 
Flood Fill mittels "Tiefensuche":
Gegeben: alte Farbe o, neue Farbe c
  • ist Farbe an der geklickten Position:
  • wenn c == o
  • tue nichts
  • sonst:
  1. gib dem aktuellen Pixel die Farbe c
  • suche alle Nachbarpixel mit Farbe o und lege sie auf einen Stack
  1. solange noch Punkte auf dem Stack sind:
  • entnehme den obersten Punkt
  • wiederhole 1.
Alternative: zeichne Farbe c bereits, wenn der Punkt auf den Stack gelegt wird. Das ist besser, weil kein Pixel zweimal auf den Stack kommt.
Stack in Python list, drauflegen list.append(point), runternehmen list.pop() 
 
def computeNeighbors(point, width, height):
    # Nachbarn ausrechnen und in Liste schreiben
    return neighbors
# Hilfsfunktion
def isInside(point, width, height):
 
def floodFill (image, point, value):
 
class FloodFill:
 
Übungsblatt 7:
  • drawCircle() und class Circle 
  • flood-fill vollenden
  • Zeichnungsobjekte vollenden und eine nette Zeichnng erstellen
  • Bonus: Bildvergrößerung und -interpolation
 

 Lektion 8

Wissenschaftliches Rechnen in Python:
 
Die Anaconda-Distribution von Python ist speziell für wissenschaftliches Rechnen optimiert. D.h. die meisten relevanten Pakete sind schon vorinstalliert.
 
  • numpy: grundlegende Funktionalität:
  • Matrix-Klasse
  • Arrays mit beliebigen Dimensionen array[i, j, k, l] 
  • numpy.linalglinear Algrebra, linear Gleichungssysteme, Eigenwerte
  • numpy.randomfortgeschrittene Zufallszahlen 
  • numpy.fft Fourier-Transformation
  • matplotlib oder pylab 
  • Zeichnen von Diagrammen
  • Anzeigen von Bildern
  • scipy: fortgeschrittene Methoden für wissenschaftliches Rechnen
  • numerische Integration
  • Diffenzialgleichungen
  • spezielle Funktionen
  • Wahrscheinlichkeitsrechnung, Verteilungsfunktionen, statistische Tests
  • ...
  • scikit-image (skimage)
  • Bilder einlesen und exportieren
  • Bildbearbeitung
  • Bildanalyse
  • scikit-learn (sklearn)
  • maschinelles Lernen
  • Klassifikation, Regression, Clusterung
 
Für wissenchaftliches Rechnen eignen sich zur Steuerung von Python besonders 'jupyter notebooks'. Wenn Sie den Befehl jupyter notebook in der Console eingeben, wird Python in einem Webserver gestartet, der standardmäßig unter der URL http://localhost:8888/ erreichbar ist (der Service kann aber auch für Remote-Zugriff konfiguriert werden). Mit Hilfe eines im Internet-Browser laufenden Notebook-Frontends kann man Python fernsteuern, und die Ergebnisse werden direkt im Notebook angezeigt. Ähnlich wie im Hackpad kann man Fließtext und Code mischen, aber der Code ist, dank des Servers im Hintergrund, direkt ausführbar.
 
Beispiele für das Erstellen von Diagrammen, die Bildanzeige und einfache Bildverarbeitungsfunktionen finden Sie in plotting-notebook.pdf
Dies ist das PDF-Transkript der Notebook-Sitzung aus der Vorlesung. Leider kann das Beispiel hier nicht als aktives Notebook verlinkt werden, weil uns kein öffentlicher jupyter-Server zur Verfügung steht.
 

Lektion 9

Testen mit unittest und nose 
 
Anmerkung: nose muss eventuell nachinstalliert werden mit python -m pip install nose oder conda install nose 
 
Erstellen der Tests
 
Bisher wurde der assert Befehl im Code verwendet. Im Allgemeinen möchte man die Implementierung von den Tests trennen. Dazu lagert man den zum Testen notwendigen Code in eine extra Datei aus, die mit dem Prefix test_ beginnen sollte.
 
In diesen Test-Dateien erstellt man eine neue Klasse, die von unittest.TestCase erbt.
 
from unittest import TestCase
 
class MyTest(TestCase):
    def test_something(self):
        # test something here
        two = self.helper_function()
        self.assertTrue(1 + two == 3)
    
    def helper_function(self):
        # do something helpful
        return 2
 
Alle Methoden mit dem Prefix test werden während eines Testlaufs automatisch ausgeführt. Andere Methoden können zusätzlich als Hilfsfunktionen definiert werden. Die Reihenfolge, in der die Testmethoden ausgeführt werden, ist dabei nicht festgelegt. Jede Testmethode hat ihre eigene Umgebung und sollte nicht darauf vertrauen, dass ein bestimmter Test vorher ausgeführt wurde.
 
Ausführen der Tests:
 
  • Aufrufen der Funktion unittest.main() im Mainblock der Test-Datei
  • Oder mit dem Befehl nosetests bzw python -m nose 
  • nosetests ohne Argument führt alle Tests im aktuellen Ordner aus (also alle Skripte, deren Namen mit test beginnt)
  • noseteststest_file.py führt nur die Tests in der Datei test_file.py aus
 
Test-Ausgabe:
 
Nach dem Ausführen der Tests erhält man eine Ausgabe, die verschiedene Symbole enthält:
  • . (Punkt) signalisiert einen bestandenen Test
  • F für einen fehlgeschlagenen Test (eine Assertion war False)
  • E falls während der Ausführung einer Testmethode ein anderes Problem (typischerweise eine unerwartete Exception) aufgetreten ist
 
Für jeden nicht-bestandenen Test erhält man außerdem die Fehlermeldung mit dem entsprechenden Traceback als Ausgabe.
Am Ende gibt es noch eine Zusammenfassung über die Anzahl aller Tests und die benötigte Zeit.
 
Setup und Teardown
 
Oft müssen für mehrere Tests die selben Initialisierungs- und Aufräumarbeiten durchgeführt werden. Dazu kann man die Methoden setUp() und tearDown() verwenden. Diese Methoden werden vor bzw. nach jedem Test ausgeführt.
 
class MyTest(TestCase):
    def setUp(self):  # the name must be setUp, nothing else
        self.my_array = [1, 2, 3]
    
    def test_my_array(self):
        self.assertEqual(len(self.my_array), 3)
    
    def tearDown(self):  # the name must be tearDown, nothing else
        print("Clean up something super important")
        
Noch mehr "asserts":
 
Der assert-Befehl, den Sie schon kennen, testet stets, ob der nachfolgende Ausdruck True ergibt. Häufig ist es aber natürlicher, die Testbedingung anders zu formulieren. Dazu gibt es in der Klasse TestCase viele zusätzliche assert-Varianten, wie z.B. assertFalse und assertEqual. Die Liste aller vorhandenen assert-Methoden befindet sich unter https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual
 
Tests mit assertRaises 
 
Um zu testen, dass eine bestimme Exception auftritt, kann die Methode self.assertRaises  verwendet werden. Man gibt dort an, welche Exception der Code auslösen sollte. Falls die erwartete Exception nicht auftritt, sondern der Code ohne Exception durchläuft, so gilt der Test als gescheitert. Wenn eine andere (also die falsche) Exception auftritt, gilt der Test ebenfalls als nicht bestanden:
 
class TestRaise(TestCase):
    def test_str_to_int(self):
        with self.assertRaises(ValueError):
            int("this is not a number")
            
    def test_sqrt_of_negative(self):
        with self.assertRaises(ValueError):
            math.sqrt(-1)
 

Wiederholung

Funktionale Programmierung
 
Funktionale Programmierung: Funktionen werden wie Variablen behandelt und können somit als Argumente an andere Funktionen übergeben werden. Damit kann man lesbareren und kompakteren Code erreichen.
 
from math import sqrt
 
# Betragsfunktion, die an der Spitze abgerundet ist
# größeres 'epsilon' = mehr Abrundung
def soft_abs(value, epsilon=1e-7):
    return sqrt(value**2 + epsilon**2)
    
array = ... # Array von Zahlen
 
# soft_abs() anwenden mit Schleife
result = []
for elem in array:
    result.append(soft_abs(elem))
 
Die Funktion map implementiert genau diese Schleife, wobei die in jeder Iteration auszuführende Funktion als Argument übergeben wird:
def my_map(function, iterable):
    result = []
    for elem in iterable:
        result.append(function(elem))
    return result
 
# verwende 'map' anstelle einer Schleife
result = my_map(soft_abs, array)
 
Zum Vergleich mit list comprehension:
 
result = [soft_abs(elem) for elem in array]
 
Man kann auch mehrere Iterables an map übergeben. Die übergebene Funktion muss dann entsprechend viele Argumente haben:
 
x_array = ... # Array von x-Koordinaten
y_array = ... # Array von y-Koordinaten
 
def norm(x, y): # auch oft 'hypot' genannt
    return sqrt(x**2 + y**2)
 
norms = map(norm, x_array, y_array) 
 
Zum Vergleich mit list comprehension und zip 
 
norms = [norm(coord[0], coord[1]) 
             for coord in zip(x_array, y_array)]
 
Zum Vergleich: Schleife
 
norms = []
for k in range(len(x_array)):
    norms.append(norm(x_array[k], y_array[k]))
 
Die Funktion reduce reduziert ein Array auf einen einzelnen Wert. Zunächst zum Vergleich die Schleife:
 
uebungspunkte = ... # Punkte pro Aufgabe als Array
 
zwischensumme = 0
for aufgabe in uebungspunkte:
    zwischensumme += aufgabe
summe = zwischensumme
 
Die Operation in der Schleife (hier: Addition) muss der reduce-Funktion als Argument übergeben werden. Dazu eignen sich besonders lambda-Funktionen:
 
from functools import reduce
 
summe = reduce(lambda zwischensumme, aufgabe: zwischensumme + aufgabe, uebungspunkte)
 
# das implementiert
zwischensumme = uebungspunkte[0]
for aufgabe in uebungspunkte[1:]:
    zwischensumme += aufgabe
summe = zwischensumme
 
Bemerkung: Das reduce mit der Addition kann auch mit der vordefinierten Funktion sum abgekürzt werden:
 
summe = sum(uebungspunkte)
 
Die ursprüngliche Schleife bekommt man, indem man reduce als letztes Argument einen Initialwert für die Zwischensumme übergibt:
 
summe = reduce(lambda zwischensumme, aufgabe: zwischensumme + aufgabe, uebungspunkte, 0) # '0' ist der Initialwert
 
lambda-Funktionen sind Abkürzungen für 'namenlose'-Funktionen, die man 'vor Ort' definieren und dann gleich wieder vergessen will.
 
def addiere(zwischensumme, aufgabe):
    return zwischensumme + aufgabe
 
Das ist äquivalent zu
 
addiere = lambda zwischensumme, aufgabe: zwischensumme + aufgabe
 
Wenn man die lambda-Funktion direkt als Argument an reduce übergibt, bekommt sie gar nicht erst einen Namen. Das ist sinnvoll, wenn man die Funktion nur einmal benutzen möchte. Entscheidend für die Auswahl, ob man Schleifen oder map/reduce bzw. explizite Funktion oder lambda-Funktionen benutzt, ist die Lesbarkeit des Codes, außer man ist gezwungen mapzu benutzen, weil man z.B. mit einem speziellen parallel_map die Ausführung automatisch parallelisieren kann.
 
Die Funktion accumulate funktioniert genauso wie reduce, aber alle Zwischenergebnisse werden wieder in ein Array gespeichert:
 
uebungspunkte = ... # Punkte pro Aufgabe als Array
 
running_total = [uebungspunkte[0]]
for aufgabe in uebungspunkte[1:]:
    running_total.append(running_total[-1] + aufgabe)
 
Die Funktion accumulate realisiert diese Schleife. Im Unterschied zu map und  reduce sind die Argumente hier vertauscht, weil die Addition als Standardwahl für die auszuführende Operation gesetzt werden soll (Beachte: nur die hinteren Funktionargumente dürfen Defaultwerte haben):
 
from itertools import accumulate
running_total = accumulate(uebungspunkte, 
     lambda zwischensumme, aufgabe: zwischensumme + aufgabe)
     
# das 'lambda' kann man weglassen, weil Addition die 
# Default-Operation ist
running_total = accumulate(uebungspunkte)
 
Schließlich die Funktion filter: Sie überträgt nur die Elemente aus einem Array, die eine gegebene Bedingung erfüllen.
 
uebungspunkte = {'mueller': 200, 'schultze': 35, ...}
 
total = 310
klausurzulassung = []
for student in uebungspunkte.keys():
    if uebungspunkte[student] / total >= 0.5:
        klausurzulassung.append(student)
 
Mit filterered list comprehension wird daraus
 
klausurzulassung = [student 
    for student, punkte in uebungspunkte.items()
    if punkte / total >= 0.5]
 
Mit filter schreibt man:
 
klausurzulassung = filter(
    lambda student: uebungspunkte[student] / total >= 0.5,
    uebungspunkte.keys())
 
Eigene Iteratoren
 
Ein 'iterable' ist ein Typ, der die Methoden __iter__ und __next__implementiert um einen Iterator zu erzeugen bzw. das nächste Element der Schleife zu erhalten. Wir implementieren als Beispiel unsere eigene Version von range:
 
class MyRange:
    def __init__(self, start, stop, step):
        # Endlosschleifen verhindern
        if step == 0:
            raise ValueError("MyRange(): step must be non-zero")
        self.current = start
        self.stop = stop
        self.step = step
        
    def __iter__(self):
        return self # MyRange ist schon ein Iterator
        
    def __next__(self):
        # Abbruchbedingungen testen
        if self.step > 0 and self.current >= self.stop:
            raise StopIteration()
        if self.step < 0 and self.current <= self.stop:
            raise StopIteration()
        # einen Schritt ausführen und den alten Wert zurückgeben
        result = self.current
        self.current += self.step
        return result
        
for k in MyRange(0, 10, 2):
    print(k)
 
Eine einfache Möglichkeit, eigene Iterables zu definieren, sind die sogenannten 'Generatoren' (generators). Das sind Funktionen, die anstelle des Schlüsselworts return das Schlüsselwort yield verwenden. Solche Funktionen werden bei aufeinanderfolgenden Aufrufen immer mit dem Befehl nach dem yield fortgesetzt (anstatt wieder am Anfang der Funktion zu beginnen) und laufen dann bis zum nächsten yield. Die yields haben dabei die selbe Funktion wie __next__ bei einem Iterator. Für Details siehe in der Python-Dokumentation unter https://docs.python.org/3.5/tutorial/classes.html#generators
 
Beispiel: von 0 bis Unendlich zählen:
# dieser Generator ist in 'itertools' bereits vorhanden
def count(start=0):
    current = start
    while True:
        yield current
        current += 1
 
Eine solche Endlosschleife muss mit dem break-Befehl beendet werden:
 
jetzt_ist_aber_schluss = 999
for k in count():
    if k == jetzt_ist_aber_schluss:
        break # Schleife abbrechen
    ... # Code ausführen, wenn noch nicht Schluss ist
 
Wiederholungsaufgaben:
Aufgabe 1: (minimum requirement)
a) Schreiben Sie eine Funktion, die das arithmetische Mittel zweier Zahlen implementiert und zurückgibt.
b) Erweitern Sie Ihre Funktion so, dass diese ein Array an Zahlen als Argument bekommt und das arithmetische Mittel davon berechnet.
 
Aufgabe 2: dictionaries
Rufen Sie sich Aufgabe 2 aus Lektion 1 aus dem Hackpad (Wochentagsformel) ins Gedächtnis und implementieren Sie die uneleganten zahlreichen if-Anweisungen mit Hilfe eines Python dictionaries.
 
Aufgabe 3: for- und while-Schleifen
Schreiben Sie eine Funktion, die die Fibonacci-Folge bis zu einer angegebenen Zahl n als Liste ausgibt mittels while.
Dann schreiben Sie noch eine Funktion, die mittels einer for-Schleife die ersten 15 Zahlen einer Fibonacci-Folge ausgibt.
 
Aufgabe 4: Dateien einlesen und schreiben
Legen Sie eine .txt-Datei an, wo Sie einen Text Ihrer Wahl eintippen (oder den Text dieser Aufgabe reinkopieren).
Schreiben Sie eine Funktion, die alle Zeichen mit Ausnahme der Vokale aus der Textdatei in eine andere Datei speichert.
 
Aufgabe 5: format-Funktion
Schreiben Sie eine Funktion die die Zahlen mit 3 Nachkommastellen formatiert. Dazu sollen die Tausender mit einem Komma getrennt werden und es sollen Nullen vor der Zahl stehen, so dass die Länge Insgesamt 13 beträgt.
a) Wenden Sie die Funktion mit map auf einem array an.
b) Wenden Sie die Funktion mittels list comprehension
c) Erstellen Sie keine extra Funktion, sondern verwenden sie eine lambda-Funktion
 
def myformat(x):
    return "{:0>13,.3f}".format(x)
 
a = [53453.56465,46548.54654,878.454,8984]
print(list(map(myformat, a)))
print([myformat(k) for k in a])
print(list(map(lambda x: "{:0>13,.3f}".format(x), a)))
 
> in Zeile 2 bewirkt, dass die Nullen nicht mehr durch Komma getrennt werden.
 
Aufgabe 6: zip(), product(), dictionaries
Lektion 4 Aufgabe 4 Kartenspiel - Skat, Stiche Zählen
Sie lösen die Aufgabe für sich selbst, nicht schummeln auf die Lösung schauen! :)
 
Aufgabe 7: accumulate(), reduce()
Importieren Sie itertools und operator. Implementieren Sie mit Hilfe von accumulate() und operator.mul die Fakultätsfunktion.
Dann mittels reduce()das Gleiche.
Keine While- oder For-Schleifen benutzen!
 
from itertools import accumulate
import operator
from functools import reduce
 
def factorial(x):
    return max(list(accumulate(range(1,x+1), operator.mul)))
def myfactorial(x):
    return reduce(lambda x,y: x*y, range(1,x+1))
assert factorial(45) == myfactorial(45)
 
Aufgabe 8: filter()
Gegeben Sei eine beliebige Liste an ganzen Zahlen.
Benutzen Sie die Funktion filter() und schreiben Sie eine Funktion, die eine Liste mit den perfekten Quadraten zurückgibt. Mit perfekten Quadraten ist gemeint, dass das Ergebnis des Wurzelziehens eine ganze Zahl bleibt.
 
from math import sqrt, floor
def perfectSquares(liste):
    return list(filter(lambda x: sqrt(x)==int(sqrt(x)), liste))
print(perfectSquares(range(10)))
 
Aufgabe 9: Klassen
Implementieren Sie eine Klasse Animal, die als Attribute folgendes nimmt:
  • de Anzahl an Beine: int
  • die Möglichkeit zu fliegen: bool
  • die Möglichkeit zu schwimmen: bool
  • den Besitz des Fells: bool
  • Name des Tieres: str
 
Implementieren Sie dazu Vergleichsoperatoren, um die Tiere auf Gleichheit (nach den obigen Attributen außer Name) prüfen zu können.
Erstellen Sie damit verschiedene Tiere und vergleichen Sie diese miteinander.
 
Implementieren Sie folgende Methoden:
is_fish(), die True zurückgibt, wenn es um einen Fisch handeln könnte
is_mammal(), analog für Säugetiere
is_bird(), analog für Vögel.
 
Implementieren sie die __str__-Methode, die den Namen des Tiers und seine Art ausgibt. Unbekannt, falls keine Art bestimmt werden konnte.
 
Aufgabe 10: Container Datentypen
Gegeben sei eine beliebige Liste mylist an Zahlen. Was für eine Möglichkeit gibt es die Anzahl der unterschiedlichen Elemente durch eine Zeile an Python-Code zu erfahren?
Tipp: Denken Sie an eine geeignete Datenstruktur... dict, list, set, class, np.ndarray...
 
print(len(set(mylist)))
 
Aufgabe 11: try, except
Schreiben Sie eine Funktion, die den Benutzer nach einer ganzen Zahl fragt. Lassen Sie ihn solange eine Eingabe vornehmen, bis er eine ganze Zahl eingibt. Verwenden Sie dazu try, except und machen Sie sich zu Nutze, dass eine ValueError auftritt, wenn man int() mit einem str() aufruft, der keine Zahl enthält.
 
def ganz():
    x = input("aa:")
    try:
        int(x)
        x[2]
    except ValueError as e:
        print(e)
        ganz()
 
Aufgabe 12: Klasse, Vergleichsoperatoren, Tests
Implementieren sie eine Klasse House, die als Attribut die Höhe in Metern hat. Schreiben Sie dazu die Vergleichsoperatoren, um Häuser sortieren zu können.
Schreiben Sie auch geeignete Tests dazu.