3Python-Programmierung

Die Unix-Shellprogrammierung ist geeignet, um einfache Aufgaben rund um das Dateisystem, die Benutzerverwaltung und Systemadministration schnell umzusetzen. Shellprogrammierung ist weniger geeignet, wenn die Aufgabe, die Sie programmieren wollen, entweder relativ komplex ist oder viel Interaktion mit anderen Programmen erfordert. Hierfür ist die Verwendung einer höheren getypten Programmiersprache sinnvoll.

Höhere Programmiersprachen, die der Shell-Programmierung noch im weitesten Sinne ähneln und die für diese Aufgaben in Betracht kämen, wären Skriptsprachen 22 wie Perl, Tcl, Ruby oder Python.Insbesondere Python eignet sich hervorragend für Programmieranfänger: Die Syntax ist einfach und klar und Python vereint die Paradigmen der prozeduralen, der funktionalen und der objektorientierten Programmierung. Besonders für Programmieranfänger sehr vorteilhaft ist die interaktive Interpreterumgebung, die Python anbietet, die Python-Shell. Ähnlich wie wir im vorherigen Kapitel das Programmieren in der Bash erlebt haben, stellt sich auch der Umgang mit Pythonkommandos in der Python-Shell dar.

Man kann dieses Kapitel deshalb als Anknüpfung an das vorige Kapitel betrachten: Python kann als flexiblerer Ersatz für Shellskripte dienen. Für die im weiteren Verlauf des Buches behandelten Konzepte, die weit über die Benutzerinteraktion mit einem Betriebssystem hinausgehen, wird die Programmiersprache Python als Grundlage dienen, nämlich für:

  1. die wichtigsten Programmierparadigmen: Prozedurale Programmierung, Funktionale Programmierung und Objektorientierte Programmierung
  2. die Programmierung mit regulären Ausdrücken
  3. die Datenpersistenz und Datenbanken
  4. das Internet und Internetprogrammierung
  5. die Parallele Programmierung

Die in diesem Kapitel dargestellte Python-Einführung bietet einen Überblick über eine Teilmenge aller Sprachkonstrukte, die Python anbietet. Diese Einführung hat weniger das Ziel eine vollständige Präsentation aller Möglichkeiten in Python zu bieten, sondern mehr, die wichtigsten Konstrukte einer höheren Programmiersprache kennen zu lernen und für die folgenden Kapitel gewappnet zu sein. Es gibt zahlreiche frei ver fügbare Quellen im Internet, die komplettere Pythoneinführungen bieten [10, 12] und einen Überblick über die verfügbaren Module 23 [9].

3.1Arbeiten mit Python

Python ist eine besonders für Programmieranfänger geeignete Programmiersprache. Im Gegensatz zur Bash-Programmiersprache werden in Python jedoch große Softwareprojekte erstellt, und viele Spezialisten im Umfeld Data Science, Forensics, Web-Development und Ingenieurwissenschaften schreiben komplexe Algorithmen mit Python. Entsprechend vielfältig sind auch die Entwicklungswerkzeuge und der ganze „Zoo“ an Bibliotheken und Versionen drumherum, die für die Python-Programmierung zur Verfügung stehen. Dieser Abschnitt gibt einen Überblick über die Entwicklungsumgebungen und die Versionsvielfalt sowie Empfehlungen, wo Sie als Programmieranfänger starten sollten und wie sie Python verwenden sollten, um die in diesem Kapitel vorgestellten Konzepte auszuprobieren und die Übungsaufgaben zu lösen.

3.1.1Python 3 vs. Python 2

Interessiert man sich dafür, Python zu installieren, so wird man mit der Frage konfrontiert, ob man Python 3.x oder Python 2.x installieren und verwenden möchte. Hierbei steht das „x “ für die Subversionsnummer. Die Python-2.x -Versionen sind alle rückwärtskompatibel.Was heißt das genau? Betrachten wir dafür zwei Python-Interpreter, einen der Python 2.x unterstützt und einen der Python 2.y unterstützt; nehmen wir weiter an, dass x < y , d.h., dass Python 2.y eine neuere Pythonversion ist. Rückwärtskompatibilität bedeutet, dass der Python-2.y -Interpreter immer auch fehlerfrei Programme ausführen kann, die für einen Python-2.x -Interpreter geschrieben wurden. Andersherum gilt dies natürlicherweise nicht: Ein älterer Python-2.x -Interpreter kann nicht alle Programme „verstehen“, die für einen neueren Python-Interpreter geschrieben wurden.

Im Jahr 2008 erschien eine neue Python-Version mit der Versionsnummer 3. Guido van Rossum, der Erfinder der Programmiersprache Python, entschied sich dafür Python grundlegender zu überarbeiten. Obwohl Python 3 sich aus Sicht des Programmieranfängers nur unwesentlich von Python 2 unterscheidet, waren die Änderungen dann doch so groß, dass eine Rückwärtskompatibiltät nicht mehr sichergestellt werden konnte. In der Folge entwickelten sich die beiden Versionsstränge Python 2.xx und Python 3.x parallel weiter, und noch bis heute sind nicht alle (aber fast alle) Python-Bibliotheken auf die Python-3-Version übertragen. Die Python-2.x -Versionen werden allerdings nur noch wenige Jahre lang weiterentwickelt und gewartet, und so ist es durchaus sinnvoll, sich als Programmieranfänger die Python-3-Version zu besorgen. Dieses Buch verwendet ausschließlich Python 3.

3.1.2Installation

Die einfachste Art, die Entwicklungsumgebung, die wir für das Erlernen von Python vorschlagen, zu installieren besteht darin, sich die Python-Distribution „Anaconda“ zu installieren. Es handelt sich um eine frei verfügbare Sammlung von Packeten und die Notebook-Umgebung (siehe Abschnitt 3.1.5 ). Anaconda ist über

beziehbar. Für dieses Buch benötigen wir die Python 3.x -Installation (zu der Zeit, in der dieses Buch geschrieben wurde, war x = 5).

Die Installation dürfte einige Zeit in Anspruch nehmen. Nach der Installation von Anaconda stehen Ihnen alle Werkzeuge, die in diesem Buch verwendet werden, zur Verfügung.

3.1.3Ein erstes Python-Programm

Wir schreiben nun unser erstes Python-Programm, öffnen hierzu die Bash und erzeugen eine Textdatei HalloWelt.py , die das Pythonkommando print enthält. Diese Datei führen wir anschließend aus. Das Zeichen $ soll hierbei das Prompt der Bash sein und andeuten, dass die folgenden Zeilen interaktiv der Bash übergeben werden sollen:

Um komplexere Python-Programme zu erstellen, ist es ratsam einen Texteditor zu verwenden, der Syntaxhervorhebungen unterstützt. Hier bieten sich etwa vi oder emacs an, oder eine sogenannte integrierte Entwicklungsumgebung (auch: IDE vom englischen Integrated Development Environment) wie Spyder oder PyCharm.

Wollen wir den String 'Hallo Welt' in der interaktiven Python-Shell ausgeben, so verfahren wir wir folgt (hierbei soll > das Prompt der Bash und >>> das Prompt der Python-Shell sein).

3.1.4Die Python-Shell

Ähnlich wie mit Shell-Befehlen unter Unix, kann man Python auch in einer interaktiven Umgebung verwenden, die zunächst einem Taschenrechner vergleichbar zu funktionieren scheint. Sie erwartet die Eingabe eines Ausdrucks, berechnet das Ergebnis dieses Ausdrucks und gibt die String-Repräsentation dieses Ausdrucks aus. Eine Umgebung, die eine solche Funktionsweise anbietet, nennt man auch REPL-Umgebung (von Read-Evaluate-Print-Loop). In die REPL-Umgebung gelangt man, indem man den Befehl python in der Kommandozeile einer Shell ausführen lässt, so wie in folgendem Beispiel:

Man kann auch ein Python-Programm in die interaktive Python-Shell laden und etwa eine in dem Skript definierte Funktion someFunction testen. Dies geschieht mittels der Option -i :

3.1.5Python Notebooks

Die Python-Shell ist ein hervorragendes Werkzeug gerade für Programmieranfänger. Sie erlaubt es, Python-Programme direkt zu testen und mit den selbst geschriebenen Funktionen zu interagieren und zu experimentieren, Objekte auf ihre Eigenschaften hin zu untersuchen und ihre Dokumentation zu lesen und zu durchsuchen. Wenn man allerdings viele Zeilen Programmcode in der Shell schreibt und später einen Teil des Codes ändern möchte, so werden die Einschränkungen der Shell offensichtlich: Man muss mittels der -Taste in der Kommandozeilen-Historie zurückblättern, und alle Programmzeilen bis auf die geänderte Programmzeile nochmals ausführen.

Schreibt man eine Skript-Datei, also eine Textdatei, die Python-Befehle enthält, hat man diesen Nachteil nicht und noch dazu den Vorteil, sich den Programmcode gemäß seiner Syntax farblich markiert anzeigen lassen zu können (Syntax-Highlighting), was die Lesbarkeit des Programm-Codes erleichtert. Syntax-Highlighting bieten alle gängigen integrierten Entwicklungsumgebungen ebenso wie die Editoren vi und emacs . Beim Arbeiten mit einer Skript-Datei verliert man jedoch auch die Möglichkeit der direkten Interaktion mit dem Programmcode, den die Python-Shell bietet.

Python Notebooks bringen die Vorteile beider Seiten zusammen: Notebooks bieten eine interaktive REPL-Umgebung, ermöglichen eine einfache Modifikation aller bisherigen Kommandos und erlauben darüberhinaus eine komfortable Möglichkeit der Dokumentation, die weit über die Möglichkeiten der üblichen Kommentar-Erstellung hinausgeht. Eine „Zelle“ in einem Notebook enthält standardmäßig Python-Code, der in der üblichen REPL-Manier ausgewertet und ausgedruckt wird; man kann jede Zelle jedoch leicht in eine Kommentarzelle umwandeln, die die Eingabe von Markdown-Text und sogar LATEX-Formeln erlaubt. Zudem können Notebooks gespeichert und entsprechend mit anderen geteilt werden. Diese Eigenschaften machen Notebooks zum idealen Werkzeug für Python-Anfänger und für die Präsentation von Programmierkonzepten. Wir empfehlen die Verwendung von Notebooks zum Experimentieren mit Python-Code und zum Lösen der in diesem Buch enthaltenen Aufgaben.

Wenn Sie Anaconda wie in Abschnitt 3.1.2 empfohlen installiert haben, dann können sie ein Notebook über die folgende Bash- oder Windows-Kommandozeile starten.

Es wird ein lokaler Webserver gestartet. Dieser kommunizert die im Notebook eingegebenen Kommandos an einen Python-Kernel, einer Instanz des Python-Interpreters, und leitet die Ausgaben dieser Interpreter-Instanz wiederum an den Webserver zurück. Das jupiter-Kommando öffnet dann ein Browser-Fenter mit der Notebook-Oberfläche. Abbildung 3.1 zeigt ein Beispiel einer solchen Oberfläche nach Eingabe einiger Kommandos und einiger Markdown-Kommentare.

Abb. 3.1. Ein Python-Notebook im Fenster eines Web-Browsers mit einigen Kommando-Zellen und einigen Dokumentations-Zellen.

Aufgabe 3.1

Öffnen Sie ein Python-Notebook und machen Sie sich mit der Funktionsweise der Benutzeroberfläche vertraut, indem Sie unter dem Menüpunkt „Help“ die „User Interface Tour“ wählen und vollständig durchlaufen. Informieren Sie sich anschließend über die Formatierung der Markdown-Zellen. Informationen darüber finden Sie ebenfalls im Menüpunkt „Help“.

Geben Sie nun die Befehle und Markdown-Kommentare so ein, dass Ihr Notebook dem aus Abbildung 3.1 gleicht.

3.2Einfache Datentypen

Die Bash kennt nur einen Datentyp, nämlich Strings. Python hingegen kennt eine ganze Reihe unterschiedlicher, teilweise auch aus einfacheren Datentypen zusammengesetzten Datentypen. In diesem Abschnitt wollen wir zunächst die einfachen Datentypen betrachten, die Python unterstützt, nämlich Zahlen, Boolesche Werte und Strings.

3.2.1Zahlen

In Python gibt es vier Zahlen-Typen: Ganzzahlen ( int ), lange Ganzzahlen mit beliebig vielen Stellen ( long int ), Gleitpunktzahlen ( float ) und komplexe Zahlen ( complex ). Beispiele für Python-Zahlen: 12, 3.141, 4.23E- 5 (Gleitpunkt-Darstellung), 0xFE (hexadezimale Darstellung), 3/4 (Bruchzahlen), 2** 100, 1 j+ 3 (komplexe Zahlen).

Aufgabe 3.2

Verwenden Sie ein Python-Notebook, um folgende Berechnungen durchzuführen:

(a)220

(b)(12 − 5i) · (4 + 3i)

(c)(2 + i)(2+i)

3.2.2Boolesche Werte

Noch einfacher strukturiert sind Boolesche Werte vom Typ bool , nämlich True und False . Sie sind auch Ergebnis von Vergleichsoperationen, wie in folgendem Beispiel gezeigt.

3.2.3Strings

Strings sind Sequenzen einzelner Zeichen. Im Gegensatz zu Listen und Dictionaries (die wir später ausführlich behandeln) sind Strings unveränderlich , d. h. ist ein bestimmter String einmal definiert, so kann er nicht mehr verändert werden. Man hat die Wahl, Strings entweder in doppelte Anführungszeichen (also: " ...") oder in einfache Anführungszeichen (also: ' ... ' ) zu setzen. Die spezielle Bedeutung der Anführungszeichen kann, ganz ähnlich wie in der Bash, mit dem Backspace (also: \) entfernt werden. Syntaktisch korrekte Python-Strings wären demnach beispielsweise:

Verwendet man dreifache Anführungszeichen (also: """ ...""" oder ' '' ... ' '' ), so kann man auch mehrzeilige Strings definieren.

Aufgabe 3.3

Geben Sie mit dem Python print -Kommando den folgende Text aus:

3.2.4Variablen

Variablen sind, genau wie in anderen Programmiersprachen auch, (veränderliche) Platzhalter für bestimmte Werte. Variablennamen müssen mit einem Buchstaben oder mit dem Zeichen „_“ beginnen und dürfen keine Leerzeichen oder Sonderzeichen (außer eben dem Zeichen „_“) enthalten. Korrekte Variablennamen sind beispielsweise i, _i, Kaese, kaese; die Zeichenketten 2dinge oder leer zeichen wären beispielsweise keine korrekten Variablennamen.

Python ist, im Gegensatz zu vielen gängigen Programmiersprachen, nicht statisch getypt; d. h. der Typ einer Variablen muss nicht schon vor der Ausführung eines Programms festgelegt sein, sondern er wird dynamisch, also während der Programmausführung, bestimmt.

Das hat den Vorteil, dass Variablen nicht deklariert werden müssen. Variablendeklarationen in anderen Programmiersprachen dienen ja hauptsächlich dazu, den Typ einer Variablen vor Programmausführung festzulegen. In Python kann man Variablen einfach ohne vorige Deklaration einen Wert zuweisen, wie in folgendem Beispiel:

Der Python-Interpreter leitet den Typ der Variablen aus der ersten Zuweisung ab. Außerdem kann die Verwendung von Variablen grundsätzlich flexibler erfolgen als bei statisch getypten Programmiersprachen. Folgendes Beispiel zeigt, dass eine Variable sogar abhängig von einer Programm-abhängigen Bedingung einen Wert eines anderen Typs erhalten kann. Das Beispiel verwendet die if -Anweisung, die später in Abschnitt 3.3.2 eingeführt wird.

3.2.5Operatoren

Die folgende Tabelle zeigt eine Auswahl an Operatoren, die Python anbietet, um Ausdrücke zu verknüpfen. Einige Operatoren verlangen, dass Ihre Parameter einen bestimmten Typ haben, etwa einen Sequenztyp, d.h. eine Sammlung von Werten in einer bestimmten Reihenfolge (z.B. Listen), oder eine Kollektion von Werten, d.h. eine Sammlung von Werten, wobei die Werte nicht notwendigerweise eine bestimmte Reihenfolge haben müssen (z.B. Mengen). Einige der Operatoren sind polymorph, d. h. auf unterschiedliche Typen anwendbar. So kann beispielsweise der Operator + sowohl auf Strings, als auch auf Integer, als auch auf Komplexe Zahlen angewendet werden. Jeder dieser Anwendungsmöglichkeiten liegt jedoch ein anderer Algorithmus zugrunde.

Der Vergleich bei Sequenzen erfolgt lexikografisch , d.h. nach den selben Prinzipien, wie wir es gewohnt sind, Wörter in einem Wörterbuch zu ordnen: Zunächst wird nach dem ersten Buchstaben (bzw. dem ersten Element der Sequenz) sortiert; sind ersten Buchstaben zweier Wörter (bzw. die ersten beiden Elemente zweier Sequenzen) identisch, so erfolgt die Sortierung nach den zweiten Buchstaben (bzw. nach den zweiten Elementen der beiden Sequenzen), usw.

Aufgabe 3.4

(a)Was hat + für eine Bedeutung für Strings?

(b)Was hat * für eine Bedeutung für Strings?

(c)Was ist der Unterschied zwischen dem Ausdruck 4 & 5 und dem Ausdruck 4 and 5?

(d)Finden Sie einen Fall, bei dem der Vergleichsoperator == und der Operator is unterschiedliche Ergebnisse liefern.

(e)Geben Sie ein Beispiel an für die Verwendung des Operators in .

3.3Grundlegende Konzepte

3.3.1Einrücktiefe

Eine grundlegende und in vielen anderen Programmiersprachen nicht gängige Eigenschaft von Python ist die Bedeutung der Einrücktiefe , also der am Zeilenanfang befindlichen Leerzeichen. Diese Leerzeichen gehören zur Syntax, und der Python-Interpreter liefert eine Fehlermeldung, wenn diese nicht richtig gesetzt sind. Anweisungen, die zusammengehören, müssen die gleiche Einrücktiefe haben.

Der Anweisungsblock, der einer if -Anweisung, while -Anweisung oder for -Anweisung folgt, wird also nicht explizit eingeklammert, wie es in vielen anderen Programmiersprachen wie C, C++ oder Java üblich ist, sondern die Anweisungen werden durch den Python-Interpreter durch ihre Einrücktiefe als zum selben Anweisungsblock gehörig erkannt.

3.3.2Kontrollfluss

Üblicherweise werden die Kommandos in einem Python-Skript einfach sequentiell, also nacheinander, beginnend von der ersten Programmzeile bis zur letzten, abgearbeitet. Es gibt jedoch einige Befehle, die diesen Kontrollfluss umleiten können, etwa wenn bestimmte Bedingungen erfüllt sind, oder während die Elemente einer Sequenz durchlaufen werden. Python bietet drei Kommandos zur Steuerung des Kontrollflusses an: die if -Anweisung, die for -Anweisung und die while -Anweisung. Diesen Anweisungen ist gemein, dass ihnen ein Anweisungsblock folgt; im Falle der if -Anweisung können es auch mehrere Anweisungsblöcke sein, falls der else -Zweig oder der elif -Zweig verwendet wird.

Betrachten wir nun die Syntax dieser Anweisungen:

Die if-Anweisung

Die if -Anweisung wählt eine aus meh- reren Anweisungsfolgen aus.Ausgewählt wird diejenige Anweisungsfolge, die zum ersten test mit wahrem Ergebnis ge- hört; die else -Klausel entspricht einem elif True .

Die while-Anweisung

Die while -Anweisung stellt die allgemeinste Schleife dar. Die erste Anweisungsfolge wird solange ausgeführt, wie test wahr ergibt. Die zweite Anweisungsfolge wird ausgeführt, sobald die Schleife normal (d. h. ohne Verwendung der break -Anweisung) verlassen wird.

Die for-Anweisung

Die for -Anweisung ist eine Schleife über Sequenzen, wie Listen, Tupel oder Strings. Pythons for -Anweisung funktioniert ähnlich der for -Anweisung der Bash: Die Anzahl der Durchläufe entspricht immer der Länge der Sequenz sequenz , und in jedem Durchlauf nimmt die Variable variable den Wert eines Elements in der Sequenz an.

Ist ein Teil der Syntax in obigen Beschreibungen in eckigen Klammern eingeschlossen (also: [...]), so bedeutet dies, dass der entsprechende Teil optional ist. Ist der in eckigen Klammern eingeschlossene Teil von einem Stern gefolgt (also: [...]*), so bedeutet dies, dass der entsprechende Teil beliebig oft (auch 0-mal) wiederholt werden kann. Beispielsweise kann der elif -Teil der if -Anweisung beliebig oft (und eben auch 0-mal) hintereinander verwendet werden.

Beispiele

Das in Listing 3.22 gezeigte Programm verwendet die while -Schleife und ein if -Kommando, um ein einfaches Ratespiel zu implementieren. Abbildung 3.2 zeigt ein Ablaufdiagramm, das den Kontrollfluss des Skripts aus Listing 3.22 veranschaulicht.

Listing 3.22. Implementierung eines einfachen Ratespiels als Beispiel für die Verwendung einer while-Schleife.
Abb. 3.2. Ablaufdiagramm, das den Kontrollfluss des Skripts aus Listing 3.22 veranschaulicht.

Die while -Schleife führt die Kommandos des folgenden Kommando-Blocks (die Kommandos bis einschließlich der drittletzten Zeile) solange aus, bis der Boolesche Ausdruck im Argument von while den Wert False annimmt. Das ist dann der Fall, wenn der erste Block des if -Kommandos ausgeführt wird, dessen letztes Kommando die Zuweisung des Booleschen Wertes False an die Variable weiter darstellt.

Beachten Sie in diesem Zusammenhang den Unterschied zwischen der Zuweisung „=“ und dem Operator „==“. Der Operator „==“ ist Teil eines Booleschen Ausdrucks, der für einen der Werte True oder False steht. Dagegen ist die Zuweisung „=“ kein Ausdruck (steht also auch nicht für einen bestimmten Wert), sondern ein Kommando, dass einer Variablen einen Wert zuweist.

Folgende im Programmbeispiel verwendete Funktionen sind noch nicht bekannt:

–Die Python-Funktion input ( string ) liest einen String von der Tastatur ein (ganz ähnlich wie das read-Kommando der Bash) und verwendet string als Eingabeaufforderung.

–Die Python-Funktion int ( ausdruck ) konvertiert ausdruck in eine int -Zahl.

Aufgabe 3.5

(a)Tippen Sie das vorige Beispiel in die Zelle eines Python-Notebooks, und führen Sie die Zelle aus.

(b)Erweitern Sie das Programm so, dass statt der Ausgabe „Die while-Schleife wurde beendet“ eine Ausgabe erfolgt, die informiert, wie oft sie geraten haben (etwa „Sie haben 6 Rate-Versuche gebraucht.“).

(c)Erweitern Sie das Programm so, dass das Ratespiel vier mal mit vier unterschiedlichen Zahlen abläuft; am Ende sollen Sie über den besten Rate-Lauf und den schlechtesten Rate-Lauf informiert werden, etwa so:

Listing 3.23 zeigt als weiteres Beispiel ein Programm, das die for -Schleife verwendet, um die Zahlen von 1 bis 4 auszugeben:

Listing 3.23. Ein Beispiel für die Verwendung der for-Schleife: Ein Programm, das die Zahlen von 1 bis 4 ausgibt.

Im Programmbeispiel wird die Python-Funktion range verwendet. Diese erzeugt eine Sequenz ganzer Zahlen im angegebenen Bereich. Eine Besonderheit in Python ist die Tatsache, dass Bereichsangaben für ganzzahlige Intervalle immer exklusive der rechten Grenze zu interpretieren sind. So erzeugt der Aufruf von range ( a , b ) alle ganzen Zahlen zwischen einschließlich a und ausschließlich b . Zu beachten ist außerdem, dass ein Aufruf von range die Sequenz nicht sofort erzeugt, sondern die Elemente nur dann erzeugt werden, wenn sie wirklich gebraucht werden. Man nennt eine solche Auswertungs-Strategie auch lazy oder nicht-strikt.

Wendet man die Funktion list ( ... ) oder tuple ( ... ) auf ein range -Objekt an, so „zwingt“ man Python, die spezifizerte Sequenz sofort in Form einer Liste zu erzeugen. Folgendes Beispiel zeigt dieses „Verhalten“ eines range -Objekts:

Was die Werte betrifft, die ein range -Objekt erzeugt, gilt also:

Optional kann man der range -Funktion auch als drittes Argument eine Schrittweite übergeben. Beispielsweise erzeugt range ( 1,9,2) eine Sequenz, die die Werte 1, 3, 5 und 7 enthält. Es gilt also in diesem Fall

Übergibt man range nur ein einziges Argument, so beginnt die Ergebnisliste bei 0. Es gilt also

Aufgabe 3.6

Schreiben Sie ein Pythonskript, das die Quadrate aller durch 3 teilbarer Zahlen zwischen 1 und 999 ausgibt.

Aufgabe 3.7

(a)Schreiben Sie ein Pythonskript, das die Summe aller Quadratzahlen zwischen 1 und 100 ausgibt.

(b)Schreiben Sie ein Pythonskript, das eine Zahl n von der Tastatur einliest und den Wert zurückliefert.

(c)Schreiben Sie ein Pythonskript, das zwei Zahlen n und m von der Tastatur einliest und den Wert zurückliefert.

Aufgabe 3.8

Schreiben Sie ein Pythonskript, das alle Primzahlen zwischen 1 und 1000 auf dem Bildschirm ausgibt.

Aufgabe 3.9

Schreiben Sie ein Pythonskript, das Ihnen die vier kleinsten perfekten Zahlen ausgibt.

Eine natürliche Zahl heißt perfekt, wenn sie genauso groß ist, wie die Summe Ihrer positiven echten Teiler (d. h. Teiler außer sich selbst). Beispielsweise ist 6 eine perfekte Zahl, da sie Summe ihrer Teiler ist, also 6 = 1 + 2 + 3.

3.3.3Schleifenabbruch

Die beiden im Folgenden vorgestellten Kommandos, break und continue geben dem Programmierer mehr Flexibilität im Umgang mit Schleifen; man sollte diese abermöglichst sparsam verwenden, denn sie können Programme schwerer verständlich und damit auch schwerer wartbar 24 werden lassen.

Mit der break -Anweisung kann man vorzeitig aus einer Schleife aussteigen; auch ein vorhandener else -Zweig wird dann nicht mehr gegangen. Ein Beispiel:

Aufgabe 3.10

Implementieren Sie das vorige Beispiel: wie verhält sich dieses Skript?

Mit der continue -Anweisung kann man die restlichen Anweisungen im aktuellen Schleifendurchlauf überspringen und sofort zum Schleifen„kopf“ springen.

3.3.4Anweisungen vs. Ausdrücke

Gerade für den Programmieranfänger ist es wichtig, sich des Unterschieds bewusst zu sein zwischen einer Anweisung und eines Ausdrucks. Eine Anweisung, tut etwas, wie etwa den Zustand des Programms bzw. Systems verändern: Dies geschieht etwa durch eine Variablenzuweisung, die Veränderung des Speicherinhalts oder die Ausführung einer Bildschirmausgabe. Ein Ausdruck dagegen repräsentiert einen bestimmten Wert durch dessen Berechnung der Zustand des Programms nicht verändert wird.

Einige Beispiele: Der Python-Code x=5+ 3 stellt eine Anweisung dar, nämlich die, der Variablen x einen Wert zuzuweisen. Die rechte Seite dieser Zuweisung, nämlich 5+3, ist dagegen ein Ausdruck, der für den Wert 8 steht. Man beachte in diesem Zusammenhang den Unterschied zwischen „=“, das immer Teil einer Zuweisung (also: eines Kommandos) ist und „==“, das einen Vergleich darstellt (also einen Wahrheitswert zurückliefert) und folglich immer Teil eines Ausdrucks ist: Der Python-Code 5==3 ist also ein Ausdruck, der für den Wert False steht.

Aufgabe 3.11

Viele Anweisungen enthalten Ausdrücke als Komponenten. Gibt es auch Ausdrücke, die Anweisungen als Komponenten enthalten?

In der interaktiven Python-Shell und in Python-Notebooks kann der Programmierer sowohl Anweisungen als auch Ausdrücke eingeben. Die Python-Shell geht aber jeweils unterschiedlich mit diesen um: Wird ein Kommando eingegeben, so führt die Python-Shell das Kommando aus. Wird dagegen ein Ausdruck eingegeben, so wird der Ausdruck zunächst ausgewertet und anschließend die String-Repräsentation des Ausdrucks ausgegeben.

Neben der in Abschnitt 3.3.2 vorgestellten if -Anweisung bietet Python auch die Möglichkeit, Ausdrücke mit if zu strukturieren.

expr 1 if condition else expr 2

Dieser Ausdruck steht für den Wert des Ausdrucks expr 1 falls der Ausdruck condition dem Wahrheitswert True entspricht, andernfalls steht dieser if -Ausdruck für den Wert des Ausdrucks expr 2 . Betrachten wir dazu folgende Beispiele:

Man sieht: Der if -Ausdruck ist ein vollwertiger Ausdruck, der an jeder beliebigen Stelle verwendet werden kann, an der Python einen Ausdruck erwartet.

Aufgabe 3.12

Welchen Wert haben die folgenden Python-Ausdrücke. Überlegen Sie erst und überprüfen sie dann mittels Eingabe der Ausdrücke in ein Python-Notebook.

Was passiert, wenn man die Klammern um den ersten if -Audsdruck weglässt?

3.3.5Funktionen

Schreiben Sie komplexere Skripte, so entspricht es einem guten Programmierstil, das Skript in kleinere einfachere Aufgabenstellungen aufzuteilen, und die einfacheren Teile dann so zusammenzusetzen, dass das gewünschte Ergebnis entsteht. Der Grund für diese Empfehlung ist, dass der Mensch kleine Aufgaben viel einfacher durchdenken, überblicken, entwerfen und programmieren kann. Die gängigste Möglichkeit, ein Programm in einfachere Teile aufzuteilen, sind Funktionen. Eine Funktion ist nichts anders als ein Block Python-Code, der durch Aufruf der Funktion ausgeführt wird. Dieser Code-Block hat im Allgemeinen einen Namen und erhält eine Liste von Parametern, denn viele Funktionen sind parametriert. Jede Funktion löst eine einfache Teilaufgabe und am Ende werden die Funktionen dann entsprechend kombiniert, meist durch Hintereinanderausführung.

Aufgabe 3.13

Ist eine Funktion eine Anweisung oder ein Ausdruck?

3.3.5.1 Definition mit dem Schlüsselwort def

Man kann Funktionen mit Hilfe des Schlüsselwortes def definieren. Viele Funktionen liefern nach Beendigung einen bestimmten Rückgabewert zurück. Diesen kann man mit dem Schlüsselwort return bestimmen. Nach Ausführung des return -Kommandos wird die Funktion verlassen.

Beispielsweise liefert die im folgenden Listing definierte Funktion getMax( x,y) die größere der beiden übergebenen Zahlen zurück:

Der Aufruf getMax( 4,10) würde also den Wert 10 zurückliefern.

Alle in einer Funktion verwendeten Variablen sind lokal , d. h. außerhalb der Funktion weder sichtbar noch verwendbar und nur innerhalb der Funktion gültig. Weist man einer bestimmten Variablen, die es im Hauptprogramm bzw. aufrufenden Programm schon gibt, einen Wert zu, so wird die Hauptprogramm-Variable dadurch weder gelöscht noch verändert. In der Funktion arbeitet man auf einer Kopie der Variablen, die von der Variablen des Hauptprogramms entkoppelt ist. Warum ist ein solches Verhalten sinnvoll? In der Informatik hat es sich bewährt, Funktionen inklusive ihrer Wirkungen und Informationen, die sie erzeugen, möglichst gut gegen andere Funktionen und gegen das Hauptprogramm „abzuschotten“. Man bezeichnet dieses Prinzip auch als „Datenkapselung“ oder „Information Hiding“. Lässt man beispielsweise den Code auf der linken Seite durch Python ausführen, so ergibt sich die auf der rechten Seite gezeigte Ausgabe:

Solange x kein neuer Wert zugewiesen wurde, wird das x aus dem Hauptprogramm verwendet; erst nach der Zuweisung wird ein „neues“ lokales x in der Funktion verwendet, die vom x des Hauptprogramms abgekoppelt ist. So wird sichergestellt, dass der Wert der Variablen x des Hauptprogramms nicht überschrieben wird und nach dem Funktionsaufruf wieder verfügbar ist.

Möchte man diesen Mechanismus umgehen und nicht auf einer lokalen Kopie arbeiten, so muss man in der Funktion die Variable x mit dem Schlüsselwort global deklarieren.

Eine Besonderheit von Python sind die sogenannten DocStrings : Die Zeile, die direkt auf die mit dem Schlüsselwort def eingeleitete erste Zeile der Funktionsdefinition folgt, kann einen ggf. mehrzeiligen String enthalten. Auf diesen String kann man über das Attribut __doc__ direkt zugreifen oder mittels der help -Funktion. Beispiel:

Aufgabe 3.14

Schreiben Sie ein Pythonskript, das Ihnen alle Zwillingsprimzahlen zwischen 1 und 1000 ausgibt. Verwenden Sie dazu eine (selbstgeschriebene) Funktion isPrim( n) , die True zurückliefert, wenn n eine Primzahl ist, und andernfalls False zurückliefert.

3.3.5.2 Definiton mit einem lambda-Ausdruck

Es gibt eine weitere Art, Funktionen zu definieren und zwar mit sogenannten lambda -Ausdrücken. Besonders dann, wenn man die Möglichkeiten der Funktionalen Programmierung in Python nutzt, weiß man die Definition einer Funktion durch einen lambda -Ausdruck zu schätzen. Das hängt damit zusammen, dass man häufig eine Funktion einmalig verwenden möchte, etwa als Übergabeparameter einer map , filter oder reduce -Funktion; siehe hierzu auch die Abschnitte 3.5.2 , 3.5.3 und 3.5.4 . Genau das ist mit einem lambda -Ausdruck mit der folgenden Syntax einfach möglich.

Dem Schlüsselwort lambda folgt eine durch Kommata getrennte Liste von Parametern, die mit einem Doppelpunkte abgeschlossen ist. Der darauffolgende Ausdruck ist der Rückgabewert der Funktion. Der lambda -Ausdruck selbst ist lediglich ein Funktionsobjekt; um den Rückgabewert zu berechnen, muss die Funktion zunächst mit Parametern aufgerufen werden.

Betrachten wir nun im Folgenden einige Beispiele:

Die erste Zeile definiert f mittels eines lambda -Ausdrucks als Funktion, die zwei Parameter x und y erwartet und deren Summe zurückliefert. Diese Definition von f ist äquivalent zur folgenden Funktionsdefinition von f2 mittels des Schlüsselworts def :

Ein mittels lambda -Ausdruck definiertes Funktionsobjekt kann auch ohne den Umweg einer Variablenzuweisung direkt als Funktion verwendet werden:

Funktionen können selbst wiederum Funktionen als Argument erwarten. Solche Funktionen nennt man auch Funktionen höherer Ordnung :

Durch die Erzeugung einer unbenannten Funktion mittels eines lambda -Ausdrucks ist es bequem möglich, einen Funktionsparameter für eine Funktion höherer Ordnung an Ort und Stelle zu erzeugen, ohne vorher mittels def eine Funktion zu definieren.

3.4Zusammengesetzte Datentypen

Python besitzt mehrere zusammengesetzte Datentypen, darunter Strings (str ), Listen (list ), Tupel (tuple ), Mengen (set ) und sog. Dictionaries (dict ), das sind Mengen von Schlüssel-Wert-Paaren, die einen schnellen Zugriff auf die Werte über die entsprechenden Schlüssel erlauben. Strings, Listen und Tupel sind sog. Sequenzen (auch Folgen oder Arrays genannt). Sequenzen sind iterierbar, d. h. man kann sie beispielsweise mittels einer for -Schleife durchlaufen.

3.4.1Listen und Sequenzen

Python-Listen sind Sequenzen von durch Kommata getrennten Werten, eingeschlossen in eckigen Klammern. Listen können beliebig geschachtelt werden, d. h. die Elemente einer Liste können wiederum Listen sein, deren Elemente wiederum Listen sein könnten, usw. Folgende Python-Werte sind beispielsweise Listen:

Indizierung

Sei S eine Variable, die ein Objekt enthält, das einen Sequenz-Typ besitzt – also beispielsweise einen String, eine Liste oder ein Tupel, dann sind die folgenden Indizierungs-Operationen auf S anwendbar:

Beispiele:

Slicing (Teilbereichsbildung)

Über Slicing kann man sich eine Teilsequenz erzeugen:

Beispiele:

Extended Slicing

Über einen zusätzlichen Parameter kann man sich die Schrittweite einstellen, mit der Elemente aus der Sequenz gezogen werden.

Beispiele:

Zuweisungen und Indizierung

Handelt es sich bei der Sequenz um eine Liste, so kann – da Listen ja veränderliche Objekte sind – auch eine Zuweisung über Slicing erfolgen. Es folgen zwei Beispiele, wie Teile von Listen mittels Zuweisungen verändert werden können.

3.4.2Allgemeine Sequenzoperationen

Folgende Funktionen sind auf alle Sequenzen anwendbar. Die meisten der hier aufgeführten Funktionen liefern Rückgabewerte zurück.

Beispiele:

Beispiele:

Beispiele:

Beispiele:

Aufgabe 3.15

Bestimmen Sie den Wert der folgenden Ausdrücke:

(a)[ range ( 1,100) [ 1] ,range ( 1,100) [ 2]]

(b)[ range ( 1,10) , range ( 10,20) ] [ 1] [ 2]

(c)[ 'Hello' ,2, 'World' ] [ 0] [ 2] + [ 'Hello' ,2, 'World' ] [ 0]

(d)len ( range ( 1,100) )

(e)len ( range ( 100,200) [ 0:50:2] )

Hinweis: Versuchen Sie es zunächst ohne die Hilfe von Python und überprüfen danach erst Ihre Überlegungen mit Hilfe des Python-Interpreters.

Aufgabe 3.16

Wie können Sie in folgendem Python-Ausdruck

auf den Wert von y zugreifen?

Aufgabe 3.17

Lösen sie die folgenden Aufgaben durch einen Python-Einzeiler:

(a)Erzeugen Sie die Liste aller geraden Zahlen zwischen 1 und 20.

(b)Erzeugen Sie die Liste aller durch 5 teilbaren Zahlen zwischen 0 und 100.

(c)Erzeugen Sie die Liste aller durch 7 teilbaren Zahlen zwischen 0 und 100; die Liste soll dabei umgekehrt sortiert sein, d. h. die größten Elemente sollen am Listenanfang und die kleinsten Elemente am Listenende stehen.

3.4.3Wichtige Operationen auf Listen

Folgende Liste zeigt eine Auswahl der wichtigsten Listenoperationen angewendet auf einer Liste L:

Beispiel:

Beispiel:

Beispiel:

Beispiel:

Beispiel:

Beispiel:

Diese Aufzählung der Operationen auf Listen ist nicht vollständig. Man kann sich alle vorhandenen Operationen eines Datentyps mit Hilfe der Python-Funktion dir ausgeben lassen. Der Aufruf

liefert eine sortierte Stringliste aller Operations-Namen zurück, die für den Datentyp list definiert sind. Detailliertere Informationen liefert die Python-Funktion help , die vergleichbar mit dem man-Kommando der Bash ist.Wünscht man detaillierte Informationen zur Syntax einer Operation so kann man das Kommando help verwenden. Mit einem Aufruf von help ( list .append) erhält man etwa die Hilfeinformation für die Operation append.

Aufgabe 3.18

Geben Sie in ein Python-Notebook das Kommando

ein. Was liefert das Kommando zurück? Wie erklären Sie sich das Ergebnis?

Aufgabe 3.19

Geben Sie ein möglichst kurzes Pythonkommando / Pythonskript an, das ...

(a)...die Anzahl der für den Datentyp dict definierten Operationen ausgibt.

(b)...die Anzahl der für den Datentyp list definierten Operationen ausgibt, die mit ' c' beginnen.

(c)...die Länge des längsten Operationsnamens der auf dem Datentyp list definierten Operationen ausgibt.

3.4.4Referenzen

Eine Zuweisung wie etwa

bewirkt im Allgemeinen nicht, dass eine neue Kopie eines Objektes y angelegt wird, sondern nur, dass x auf den Teil des Hauptspeichers zeigt, an dem sich y befindet. Man sagt auch, dass x und y Referenzen sind, die auf das selbe Objekt verweisen. Dies gilt immer für Werte zusammengesetzter Datentypen wie beispielsweise Listen, nicht jedoch für einfache Datentypen wie Zahlen oder Boolesche Werte. Normalerweise braucht sich der Programmierer darüber keine Gedanken zu machen. Es gibt jedoch immer wieder knifflige Situationen, in denen es von Vorteil ist, sich dieser Tatsache bewusst zu sein. Ein einfaches Beispiel:

Will man, dass b auf eine Kopie der Liste a verweist und b nicht nur, wie oben, ein neuer Zeiger auf die gleiche Liste darstellt, dann kann man dies folgendermaßen bewerkstelligen:

Dabei ist in obigem Fall a[:] dasselbe wie a [ 0:2] (siehe Seite 70 zur Erklärung der Indizierungsoperatoren) und bewirkt, dass auf der rechen Seite der Zuweisung eine Kopie der Liste erzeugt wird.

Aufgabe 3.20

Was ist der Wert der Variablen a, b und c nach der Eingabe der folgenden Kommandos in den Python-Interpreter:

3.4.5Tupel

Tupel sind Listen sehr ähnlich, mit dem Unterschied, dass sie unveränderlich sind, genauso wie auch Strings. Man kann Tupel also immer dann verwenden, wenn man sicher davon ausgehen kann, dass eine bestimmte Sammlung von Werten sich nicht verändern wird. Tupel werden in normalen runden Klammern notiert. Tupel können genauso wie andere Sequenzen auch indiziert werden. Beispiele:

Versucht man einen Teil des Tupels zu verändern, so wird eine Fehlermeldung ausgegeben:

Tupel können zusammen mit Strings verwendet werden, um Objekte formatiert in Strings einzufügen, wie in folgendem Beispiel gezeigt:

Python wandelt hier jeden Teil des nach dem %-Zeichen übergebenen Tupels in einen gemäß den Formatangaben entsprechenden String um, und setzt diesen dann anstelle der Formatangaben ein.

3.4.6Dictionaries

Ein Dictionary-Objekt stellt eine Repräsentation einer Zuordnung von Schlüsseln auf Werte dar. Diese Datenstruktur ermöglichst einen sehr schnellen Zugriff auf Werte, die über einen Schlüssel gesucht werden; der Zugriff ist auch dann noch schnell, wenn die Datenstruktur viele Millionen Einträge enthält. Ein intuitives Anwendungsbeispiel ist ein Adressbuch, das bestimmte Namen – in diesem Falle die Schlüssel – auf Adressen – in diesem Falle die Werte – abbildet. Ein Dictionary-Objekt sollte die folgenden drei Operationen effizient unterstützen: 1. Das Einfügen eines neuen Wertes w mit dem Schlüssel s. 2. Das Finden eines bestimmten Wertes w anhand seines Schlüssels s. 3. Das Löschen eines Schlüssels s zusammen mit dem zugehörigen Wert w.

Aufgrund der Tatsache, dass der Informatiker eine effiziente Unterstützung der Dictionary-Operationen häufig benötigt, bietet Python hierfür einen eigenen internen Typ dict an. Während Listen in eckigen Klammern und Tupel in runden Klammern notiert werden, werden Dictionary-Objekte in geschweiften Klammern geschrieben:

Ein einfaches Dictionary-Objekt, das eine Zuordnung von Namen auf Emailadressen realisiert, könnte folgendermaßen definiert werden:

In diesem Fall sind die Strings 'Carlo' , 'Hannes' und 'Madita' die Schlüssel und die Strings, die die Emailadressen enthalten, sind die Werte.

DieOperationen „Einfügen“ und „Suchen“werden über den Indizierungsoperator [ ... ] angesprochen, so dass sich die Verwendung eines Dictionary-Objektes teilweise anfühlt wie die Verwendung eines Listen- oder Tupelobjekts. In folgendem Beispiel wird der dem Schlüssel 'Hannes' zugeordnete Wert gesucht:

Ein neuer Eintrag kann einfach mittels einer Zuweisung erzeugt werden. Der neue Schlüssel wird mittels Indizierungsoperator angegeben.

Auch das Überschreiben eines bestehenden Eintrags ist in dieser Weise möglich:

Die Funktion del implementiert die Löschfunktion. So kann man Einträge mit einem bestimmten Schlüsselwert aus dem Dictionary löschen.

Man sieht in vorigem Beispiel, dass man viele für Sequenzen definierte Funktionen anwenden kann.

Die Typen von Schlüsseln und Werten können beliebig gewählt werden und sich auch innerhalb eines Dictionary-Objekts unterscheiden. So könnte man beispielsweise folgende Einträge dem Objekt ab hinzufügen:

Wichtig zu wissen ist, dass man nur unveränderliche Werte als Schlüssel verwenden kann, also insbesondere keine Listen:

Intern ist die Dictionary-Struktur als Hashtabelle organisiert, und nur Python-Objekte, die „hashable“ sind, eignen sich für Schlüssel eines Dictionary-Objekts.

Aufgabe 3.21

Erklären Sie, was das Problem wäre, wenn man auch veränderliche Werte (wie beispielsweise Listen) als Schlüssel in einem Dictionary-Objekt zulassen würde.

Die Reihenfolge, in der Elemente in einem Dictionary-Objekt angeordnet sind, ist nicht spezifiziert. Dennoch sind Dictionary-Objekte iterierbar:

Im Folgenden geben wir noch einige häufig verwendete Operationen über Dictionary-Objekte an. Die Variable D bezeichnet im Folgenden immer ein Dictionary-Objekt.

Beispiel:

Beispiel:

Beispiel:

Aufgabe 3.22

Gegeben sei ein Dictionary-Objekt d. Schreiben Sie eine Python-Funktion flip_dict, die ein neues Dictionary-Objekt zurückliefert, bei dem die Werte aus d den entsprechenden Schlüsseln aus d zugeordnet sind. Beispiel:

3.4.7Strings (Fortsetzung)

Häufig gebraucht, sowohl für große Programmierprojekte als auch für viele kleine nützliche Skripts, sind Funktionen, die Strings verändern, erzeugen oder durchsuchen. Häufig sind diese auch im Zusammenhang mit Dateimanipulation relevant (siehe Abschnitt 3.6 ).

Wie schon in Abschnitt 3.2.3 erwähnt, sind Strings, genau wie Listen und Tupel, Sequenzen. Man kann daher auch alle Sequenzoperationen, wie Indizierung, Slicing usw. (siehe Abschnitt 3.4.1 ) auf sie anwenden. Im Gegensatz zu Listen sind Strings aber unveränderlich, d. h. ein einmal definierter String kann nicht verändert werden. Man kann also weder einzelne Zeichen aus ihm herauslöschen (wie dies etwa die Listenfunktion del macht), noch kann man an einen bestehenden String Zeichen anfügen (wie dies die Listenmethode append macht).

Im folgenden stellen wir die wichtigesten String-Kommandos vor, gruppiert in die Abschnitte „Durchsuchen“, „Aufteilen, Zusammenfügen“ und „Formatieren“. Die Variable S steht in den Beispielen stets für ein String-Objekt.

Suchen

Beispiel:

Beispiel:

Beispiel:

Beispiel:

Aufteilen und Zusammenfügen

Beispiel:

Beispiel:

Beispiel:

Formatieren

Aufgabe 3.23

Schreiben Sie eine Python-Funktion zipString, die zwei Strings als Argumente übergeben bekommt und einen String zurückliefert, der eine „verschränkte“ Kombination der beiden übergebenen Strings ist. Beispielanwendungen der Funktion:

Aufgabe 3.24

Schreiben Sie eine Python-Funktion wordsLower, die einen String übergeben bekommt und einen String zurückliefert, in dem alle Wörter kleingeschrieben sind. Beispielanwendung der Funktion:

3.5Funktionale Programmierung

Viele der gängigen Programmiersprachen, wie C, C++, Java, C#, sind Imperative Programmiersprachen . Das Paradigma der Funktionalen Programmierung unterscheidet sich vom für die meisten Programmierer gewohnten Paradigma der Imperativen Programmierung. In der Imperativen Programmierung verwendet man überwiegend Anweisungen 25 , die etwas „tun“, d. h. den Zustand des Programms bzw. des Speichers bzw. den Zustand von Peripheriegeräten (wie etwa des Bildschirms) verändern – etwa indem sie etwas auf dem Bildschirm ausgeben, den Wert einer Variablen verändern oder einen Teil des Hauptspeichers oder eine Datei manipulieren. Auch for - oder while -Schleifen sind typische Anweisungen: In jedem Schleifendurchlauf verändert sich der Zustand der Schleifenvariablen.

In der funktionalen Programmierung verwendet man überwiegendAusdrücke , die strenggenommen nichts „tun“, sondern lediglich für einen bestimmten Wert stehen, etwa einen Integerwert, einen String oder eine Liste. Im Gegensatz zu Anweisungen verändern Ausdrücke den Zustand von Objekten, des Speichers oder der Peripheriegeräte nicht. Viele Programmierfehler entstehen dadurch, dass der Programmierer den Überblick über die durch das Programm erzeugten Zustände verloren hat. Programmiert man dagegen hauptsächlich mit Ausdrücken, so schließt man diese Fehlerquelle aus. Entsprechend lohnt es sich immer in Erwägung zu ziehen, eine imperative Schleife durch eine Listenkomprehension, eine map -Anweisung oder eine filter -Anweisung zu ersetzen.

Python ist keine rein funktionale Sprache, und ein Pythonprogrammierer wird immer auch Anweisungen verwenden müssen. In diesem Abschnitt stellen wir mit den Listenkomprehensionen und den Funktionen map , filter , reduce und enumerate die wichtigsten Werkzeuge vor, um mit weniger Anweisungen und weniger Zustandsveränderungen auszukommen.

3.5.1Listenkomprehensionen

Listenkomprehensionen sind Ausdrücke, keine Kommandos, und stehen also für einen bestimmten Wert. Man kann Listenkomprehensionen als das funktionale Pendant zur imperativen Schleife betrachten. Sie sind insbesondere für Mathematiker leicht verständlich, denn sie besitzen eine mit der Mengennotation, den „set comprehensions“ der Mathematik, vergleichbare Notation. Die Menge

{ 2 · x | x ∈ { 1, . . . 20} , x durch 3 teilbar }

entspricht 26 hierbei der Python-Listenkomprehension

Jede Listenkomprehension besteht mindestens aus einem in eckigen Klammern [ ... ] eingeschlossenen Ausdruck, gefolgt von einer oder mehreren sogenannten for -Klauseln. Jede for -Klausel kann optional durch eine if -Klausel eingeschränkt werden.

[ ausdr for variable1 in sequenz1 [if bedingung1 ]

for variable2 in sequenz2 [if bedingung2 ] ...]

Der Bedingungsausdruck einer if -Klausel hängt im Allgemeinen ab von einer (oder mehreren) durch vorangegangene for -Klauseln gebundenen Variablen. Dieser Bedingungsausdruck filtert all diejenigen Variablen der jeweiligen Sequenz aus, für die er den Wahrheitswert False liefert, oder anders ausgedrückt: Die jeweilige Variable nimmt nur diejenigen Werte der jeweiligen Sequenz an, für die der Bedingungsausdruck den Wahrheitswert True ergibt.

Der Wert der Listenkomprehension ist die Liste aller Werte des Ausdrucks ausdr für alle Kombinationen von Werten von variable1 , variable2 , usw.

Abbildung 3.3 veranschaulicht, wie sich die Werte einer Listenkomprehension mit einer for -Klausel und einer if -Klausel zusammensetzen.

Abb. 3.3. Darstellung, wie sich die Werte einer Listenkomprehension mit einer for -Klausel und einer if -Klausel zusammensetzen. Die Ausdrücke sequenz1 , bedingung1 und ausdr beziehen sich hier auf die entsprechenden Platzhalter, die in obiger Syntaxbeschreibung verwendet wurden. Wie man sieht, ist der Wert der Listenkomprehension immer eine Liste, deren Elemente durch Auswertung von ausdr in Abhängigkeit der einzelnen Elemente der Liste sequenz1 entstehen.

Beispiele

Um mit Listenkomprehensionen vertrauter zu werden, präsentieren wir im folgenden einige Beispiele. Diese zeigen jeweils eine Listenkomprehension, die mit Python ausgewertet wird.

Wir gehen in vielen der präsentierten Beispiele darauf ein, welchen Wert die einzelnen Platzhalter der obigen Syntaxbeschreibung haben, d. h. wir geben häufig der Klarheit halber an, was der jeweilige Wert der Platzhalter ausdr , ausdr1 , sequenz1 , bedingung1 , usw. ist.

i) Erzeugung einer Liste aller Quadratzahlen von 12 bis 52 :

Der Wert des Platzhalters ausdr in obiger Syntaxbeschreibung entspricht hier dem Ausdruck x* x. Die Sequenz sequenz1 entspricht range ( 1,6) . Für jeden Wert in range ( 1,6) , also für jeden Wert in [ 1,2,3,4,5] , wird ein Listeneintrag der Ergebnisliste durch Auswertung des Ausdrucks x* x erzeugt. Ergebnis ist also [ 1* 1, 2* 2, ... ] . Die folgende Abbildung veranschaulicht dies nochmals:

ii) Erzeugung einer Liste aller durch 3 oder durch 7 teilbaren Zahlen zwischen 1 und 20:

Der Wert des Platzhalters ausdr entspricht hier dem nur aus einer Variablen bestehenden Ausdruck x. Die Sequenz sequenz1 entspricht range ( 1,20) . Der Bedingungsausdruck bedingung1 entspricht x%3==0 or x%7==0. Hier wird also eine Liste erzeugt, die aus Einträgen x in der Sequenz range ( 1,20) besteht, für die der Ausdruck x%3==0 or x%7==0 den Wert True ergibt.

Aufgabe 3.25

(a)Schreiben Sie eine Python-Funktion teiler( n) , die die Liste aller Teiler einer als Parameter übergebenen Zahl n zurückliefert. Verwenden Sie zur Erzeugung der Liste der Teiler eine Listenkomprehension. Beispielanwendung:

(b)Geben Sie unter Verwendung der Funktion teiler einen Python-Ausdruck an, der eine Liste aller Zahlen zwischen 1 und 1000 ermittelt, die genau 5 Teiler besitzen.

(c)Geben Sie unter Verwendung der Funktion teiler einen Python-Ausdruck an, der diejenige Zahl zwischen 1 und 1000 ermittelt, die die meisten Teiler besitzt.

iii) Erzeugung einer Liste aller möglichen Tupel von Zahlen aus 1 bis 10.

Der Platzhalter ausdr entspricht in diesem Fall dem Tupel ( x,y) , der Platzhalter sequenz1 entspricht range ( 1,10) und der Platzhalter sequenz2 entspricht range ( 1,10) . Man sieht: Es können beliebig viele for -Klauseln hintereinander stehen, was einer Schachtelung von for -Schleifen entspricht. Im ersten Durchlauf hat x den Wert 1 und y durchläuft für diesen einen festen Wert von x die Zahlen von 1 bis (ausschließlich) 10. Im zweiten Durchlauf hat x den Wert 2 und y durchläuft wiederum die Zahlen von 1 bis ausschließlich 10, usw. Jede dieser beiden for -Klauseln könnte (auch wenn dies in obigem Beispiel nicht der Fall ist) ein if -Statement verwenden, das die Werte für x bzw. y einschränkt.

iv) Erzeugung der jeweils ersten Zeichen von in einer Liste befindlichen Strings:

Der Platzhalter ausdr entspricht hier dem Ausdruck x[ 0] , und der Platzhalter sequenz1 entspricht der Stringliste [ 'alt' , 'begin' ,... ] . Die Schleifenvariable x durchläuft nacheinander die Strings 'alt' , ' begin' , usw. In jedem Durchlauf wird das erste Zeichen des jeweiligen Strings in die Ergebnisliste eingefügt. Die folgende Abbildung veranschaulicht dies nochmals:

Aufgabe 3.26

Verwenden Sie eine Listenkomprehension, um eine Liste zu erzeugen, die alle Methoden- und Attributnamen der Klasse str enthält, die ...

(a)mit 'a' beginnen.

(b)mit 'er' enden.

(c)mehr als 10 Zeichen enthalten.

(d)den String 'cod' als Teilstring enthalten.

Tipp : Mit dir ( str ) erhalten Sie die Liste aller Methoden- und Attributnamen der Klasse str . Für obige Aufgaben benötigen Sie evtl. die String-Methoden startswith, endswith, die Funktion len , und die Operation in .

Aufgabe 3.27

Verwenden Sie eine Listenkomprehension, die ...

(a)eine Liste der Längen aller Methoden- und Attributnamen der Klasse str erzeugt.

(b)die Länge des längsten Methoden- oder Attributnamens der Klasse str berechnet.

(c)den Namen des längsten Methoden- oder Attributnamens der Klasse str zurückliefert.

v) Erzeugung der Liste aller Wörter aus einer Liste von Strings, die weniger als 3 Zeichen enthalten:

Diese Listenkomprehension besteht aus zwei geschachtelten for -Schleifen. Die äußere for -Schleife läuft über die in strlst enthaltenen Strings; s hat also im ersten Durchlauf den Wert 'Ich bin klein' , imzweiten Durchlauf den Wert 'und so fein' und im dritten Durchlauf den Wert 'ja das ist eben so' . Schauen wir uns den ersten Durchlauf der äußeren Schleife näher an: In diesem ersten Durchlauf hat s den Wert 'Ich bin klein' und w läuft über die Liste

Die Funktion split teilt einen String nach einem bestimmten Vorkommen eines Zeichens auf, in diesem Fall standardmäßig nach Leerzeichen; der Ausdruck hat also den Wert [ 'Ich' , 'bin' , 'klein' ] und die innere Schleife läuft über die drei Strings 'Ich' , 'bin' und 'klein' . Von diesen Wörtern w werden nur diejenigen „durchgelassen“, die die Bedingung len ( w) <3 erfüllen. Im ersten Durchlauf der äußeren Schleife sind das gar keine, im zweiten Durchlauf ist das das Wort 'so' und im letzten Durchlauf sind es die Wörter 'ja' und 'so' .

Aufgabe 3.28

Gegeben Sei ein langer String, der '\n' -Zeichen (also Newline-Zeichen, oder Zeilentrenner-Zeichen) enthält. Geben Sie – evtl. unter Verwendung einer Listenkomprehension – einen Ausdruck an, der ...

(a)die Anzahl der Zeilen zurückliefert, die dieser String enthält.

(b)alle Zeilen zurückliefert, die weniger als 5 Zeichen enthalten.

(c)alle Zeilen zurückliefert, die das Wort 'Gruffelo' enthalten.

Aufgabe 3.29

Programmieren Sie mit Hilfe einer Listenkomprehension eine Funktion prims( n) , die die Liste aller Primzahlen bis n zurückliefert.

Anmerkung : Häufig sehe ich, dass Programmieranfänger die Ergebnisse mit der print -Funktion „zurückgeben“. Eine Funktion, die Ergebnisse auf dem Bildschirm ausgibt, ist jedoch im Allgemeinen als Teil eines größeren Prorgammes wenig nützlich. Eine Funktion, die Ergebnisse mit Hilfe von return zurückliefert, kann dagegen ihre Ergebnisse an andere Funktionen weitergeben.

Aufgabe 3.30

Geben Sie eine Listenkomprehension an, mit der Sie die Liste aller Quersummen der Zahlen von 1 bis 1000 ausgeben können.

Aufgabe 3.31

Erzeugen Sie eine Liste aller Perfekten Zahlen zwischen 1 und 10000 mit Hilfe einer Python-Listenkomprehension.

Ein Zahl heißt perfekt, wenn Sie gleich der Summe ihrer positiven echten (d. h. die Zahl selbst gehört nicht dazu) Teiler ist. Beispielsweise ist 6 eine Perfekte Zahl, denn 6 = 1 + 2 + 3.

Aufgabe 3.32

Wir wollen Zufallsexperimente mit einem 6-seitigen Würfel durchführen. Sie können hierzu die Funktion randint aus der Bibliothek random verwenden und diese folgendermaßen in ein Python-Notebook oder ein Python-Skript einbinden:

(a)Erzeugen Sie mittels einer Listenkomprehension eine Liste mit 10 zufälligen Werten zwischen 1 und 6 – um einen 10-maligen Wurf mit einem 6-seitigen Würfel zu simulieren.

(b)Berechnen Sie eine Schätzung der Wahrscheinlichkeit, dass bei einem 10-maligen Wurf keine 1 fällt.

Zählen Sie zur Beantwortung dieser Frage die 10er-Würfe von (sagen wir) 100000 10er-Würfen, die keine 1 enthalten. Realisieren Sie dies durch eine Listenkomprehension.

(c)Berechnen Sie eine Schätzung der Wahrscheinlichkeit, dass bei einem 10-maligen Wurf genau drei mal eine 1 fällt.

3.5.2Die map-Funktion

Die map -Funktion verknüpft mehrere Listen elementweise mit einer als Parameter übergebenen Funktion:

map ( fkt , sequenz 1 , sequenz 2 ,...)

Die map -Funktion ist eine sog. Funktion höherer Ordnung (engl.: higher-order function ); das ist eine Funktion, die als Parameter eine Funktion erwartet oder eine Funktion als Ergebnis liefert.

Funktionsweise: Die map -Funktion ruft die Funktion fkt für alle ersten Elemente der Listen sequenz 1 , sequenz 2 , ... auf, anschließend für alle zweiten Elemente, usw. Die jeweils erhaltenen Werte werden als spezielle Sequenz vom Typ map zurückgeliefert. Diese Sequenz wird nicht sofort ausgewertet, sondern erst dann, wenn die Elemente explizit angefordert werden, etwa durch eine for -Schleife, oder wenn man Python durch die Funktion list ( ... ) zwingt, das Ergebnis in eine Liste zu überführen. Dieses Verhalten ist besonders dann sinnvoll, wenn es sich um sehr lange Se quenzen handelt, die bei einer sofortigen Auswertung viel Speicherplatz benötigen würden.

Folgendes Beispiel veranschaulicht die Funktionsweise der map -Funktion, die mit einer zweistelligen Funktion f zwei Listen [x 0 , x 1 , . . .] und [y 0 , y 1 , . . .] elementweise verknüpft und daraus eine neue Sequenz e 0 , e 1 , . . . erzeugt:

Der Aufruf von map in der ersten Zeile erzeugt ein Sequenz-Objekt, das die Ergebnisse zunächst aber noch nicht auswertet und dessen String-Repräsentation lediglich die Information enthält, dass sich das Objekt an Stelle 0xde31f5dba8 befindet. Erst nachdem wir das Objekt durch den Aufruf list ( m) dazu „zwingen“, berechnet es seine Ergebnisse und liefert diese in einer Liste zurück. Etwas eigenartig mag das Verhalten des letzten Aufrufs erscheinen: Hat das map -Objekt einmal seine Inhalte ausgewertet, ist es „erschöpft“ und kann keine Werte mehr produzieren.

Die map -Funktion kann natürlich auch mit einer Liste und einer einstelligen Funktion verwendet werden, wie in folgendem Beispiel gezeigt:

Der an die map -Funktion übergebene lambda -Ausdruck ist einstellig, d. h. er erwartet genau einen Parameter, der eine (indizierbare) Sequenz sein muss, und liefert die ersten beiden Elemente davon zurück.

Berechnungen, die eine map -Funktion verwenden, die mit einer einstelligen Funktion und einer einzigen Liste arbeiten, können auch mittels einer Listenkomprehensionen ausgedrückt werden. Obiges Beispiel, unter Verwendung von Listenkomprehensionen, hat die folgende Form:

Ein Unterschied ist, dass man bei Verwendung von map die Kontrolle hat, wann die erzeugte Sequenz ausgewertet wird; eine Listenkomprehension berechnet immer sofort die gesamte Liste.

Aufgabe 3.33

Verwenden Sie die map -Funktion, um einer (String-)Liste von Zeilen Zeilennummern hinzuzufügen. Der Ausdruck

sollte also eine Sequenz erzeugen, die die folgenden Einträge hat:

3.5.3Die filter-Funktion

Mit Hilfe der filter -Funktion können bestimmte Elemente einer Liste oder Sequenz ausgefiltert werden, wenn sie eine bestimmte Eigenschaft nicht erfüllen.

filter ( function , sequenz )

Die filter -Funktion liefert nur diejenigen Elemente von sequenz als Liste zurück, für die das Funktionsargument function , angewandt auf das jeweilige Sequenzelement, den Wert True zurückliefert. Die übergebene Funktion muss einstellig sein und als Rückgabewert einen Booleschen Wert erzeugen. Ein Aufruf von filter liefert ein Objekt vom Typ filter zurück, das eine Sequenz darstellt, die ihren eigentlichen Inhalt erst dann auswertet, wenn es der Programmierer explizit verlangt. Das filter -Objekt hat – abgesehen vom eigentlichen Namen des Typs – die gleichen Eigenschaften, wie das im vorherigen Abschnitt besprochene map -Objekt.

Beispielsweise kann man folgendermaßen alle Zahlen zwischen 2 und 25 erhalten, die weder durch 2 noch durch 3 teilbar sind:

Berechnungen, die eine filter -Funktion verwenden, können immer auch mit einer Listenkomprehension ausgedrückt werden, mit dem Unterschied jedoch, dass Listen komprehensionen eine fertig ausgewertete Liste produzieren. Obiges Beispiel unter Verwendung einer Listenkomprehension hat die folgende Form:

Aufgabe 3.34

Verwenden Sie filter und/oder map , um ...

(a)sich alle Methodennamen des Typs string ausgeben zu lassen, die kürzer als 6 Zeichen sind.

(b)sich die Länge des längsten Methodennamens des Typs string ausgeben zu lassen.

(c)zu zählen, wie viele Methodennamen des Typs list mit 'a' beginnen.

(d)zu zählen, wie viele Methodennamen des Typs string ein 'a' enthalten und mehr als 5 Zeichen lang sind.

(e)sich die Länge des längsten Methodennamens des Typs string ausgeben zu lassen, der ein 'a' enthält.

Hinweis: Die Liste aller Methodennamen von beispielsweise string erhält man mit dem Pythonkommando dir ( string) .

Aufgabe 3.35

Wir wollen alle möglichen Wörter mit bestimmten Eigenschaften erzeugen. Gegeben seien die folgenden Definitionen:

(a)Schreiben Sie eine Pythonfunktion woerter( n) , die alle Wörter der Länge n

mit Buchstaben aus A erzeugt.
Tipp : Zur Lösung kommt man elegant rekursiv, indem man überlegt, was welchen Wert woerter ( 0) und woerter ( 1) haben muss und anschließend überlegt, wie man woerter ( n) aus woerter ( n-1) berechnen kann.

(b)Schreiben Sie eine Pythonfunktion w1d( n) , die alle Wörter der Länge n mit Buchstaben aus A erzeugt, die mindestens eine Ziffer enthalten.

(c)Schreiben Sie eine Pythonfunktion wdE( n) , die alle Wörter der Länge n mit Buchstaben aus A erzeugt, die mit einer Ziffer enden.

Tipp : c.isdigit() testet, ob das Zeichen c eine Ziffer ist.

3.5.4Die reduce-Funktion

Neben der map - und der filter -Funktion, ist die reduce -Funktion eine weitere im Rahmen der Funktionalen Programmierung häufig verwendete Funktion höherer Ordnung.Während in der Python 2.x -Version die reduce -Funktion noch initial verfügbar war, befindet sich diese in den Python 3.x -Versionen in dem Modul functools und muss mittels

in den Namensraum geladen werden.

Die reduce -Funktion verknüpft die Elemente einer Liste bzw. einer Sequenz nacheinander mit einer zwei-stelligen Funktion.

reduce ( function , sequenz )

Die Verknüpfung erfolgt von links nach rechts. Um die Funktionsweise von reduce zu veranschaulichen gehen wir von folgender Definition einer zweistelligen Funktion aus – das Symbol ist hierbei Platzhalter für eine beliebige Verknüpfung der beiden Parameter x und y:

Dann wertet sich der folgende Ausdruck

Beispiele

Wir geben im Folgenden einige Beispiele für die Verwendung der reduce -Funktion:

i) Aufsummieren aller ungeraden Zahlen von 1 bis 1000

Berechnet die Summe (· · · ((1 + 3) + 5) + . . . + 999). Die gleiche Berechnung kann man auch mit sum ( range ( 1,1000,2) ) durchführen.

ii) Verknüpfen einer Menge von Strings zu einem String der aus einer Menge von Zeilen besteht

Wie schon in Abschnitt 3.2.5 angedeutet, ist der + -Operator überladen, d. h. seine Funktionsweise ist abhängig vom Typ der Operanden. Sind die Operanden Zahlen, so entspricht der + -Operator einer Addition; sind die Operanden dagegen Sequenzen, dann entspricht der + -Operator einer Konkatenation, d. h. einem Zusammenketten von Sequenzen. Die Funktion f ist so definiert, dass sie zwei Strings mit dem Newline-Zeichen '\n' als Trenner zusammenkettet. Die reduce -Funktion verkettet entsprechend alle Strings in der Liste und fügt ein ' \n' -Zeichen zwischen jeweils zwei Strings ein.

iii) Umwandeln einer als String repräsentierten Hexadezimal-Zahl in einen Python Integerwert.

Wir gehen davon aus, dass die in der folgenden Aufgabe durch den Leser zu definierenden Funktion c2h eine als Zeichen repräsentierte hexadezimale Ziffer in den entsprechenden int -Wert umwandelt. Die Listenkomprehension innerhalb der reduce -Funktion erzeugt die Liste

[1 * 165 , 2 * 164 , 15 * 163 , 11 * 162 , 3 * 161 , 10 * 160 ]

Diese Werte müssen nur noch aufsummiert werden – dies erledigt die reduce -Funktion – und man erhält den Wert

und dies entspricht genau der Dezimalrepräsentation der als String repräsentierten hexadezimalen Zahl ' 12fb3a' .

Aufgabe 3.36

Schreiben Sie eine Funktion c2h( c) , die das Zeichen c in einen int -Wert umwandelt, falls c in '0123456789abcdefABCDEF' , d. h., falls c eine hexadezimale Ziffer repräsentiert. Es soll beispielsweise gelten:

iv) Umwandeln einer als String repräsentierten Hexadezimal-Zahl in einen Integerwert unter Verwendung des Horner-Schemas :

Nehmen wir an es sei eine hexadezimale Zahl h 0 h 1 h 2 h 3 h 4 gegeben. Will man daraus die entsprechende Dezimalzahl über

h 0 * 164 + h 1 * 163 + h 2 * 162 + h 3 * 161 + h 4 * 160

berechnen, so ist dies wenig effizient. Der Grund dafür ist, dass zur Berechnung der Potenzen sehr viele (nämlich 4+3+2) Multiplikationen durchgeführt werden müssen, und Multiplikationen sind recht rechenintensiv. Die gleiche Berechnung kann folgendermaßen mit nur vier Multiplikationen durchgeführt werden:

(((h 0 * 16 + h 1 ) * 16 + h 2 ) * 16 + h 3 ) * 16 + h 4

Dieses Berechnungs-Schema ist das sog. Horner-Schema . Eine Implementierung kann elegant mit Hilfe der reduce -Funktion folgendermaßen erfolgen:

Die Listenkomprehension [ c2h( h) for h in hexNum] erzeugt zunächst eine Liste der Integerwerte, die den einzelnen Ziffern in hexNum entsprechen – hier wäre das die Liste [ 1,2,15,11,3,10] . Die reduce -Funktion verknüpft dann die Elemente der Liste mit der Funktion f und verwendet so das Horner-Schema, um die Dezimalrepräsentation der Hexadezimalzahl ' 12fb3a' zu berechnen.

Aufgaben

Aufgabe 3.37

Verwenden Sie das mittels reduce -Funktion implementierte Horner-Schema, um eine als String repräsentierte Binärzahl in die entsprechende Dezimalzahl umzuwandeln.

Aufgabe 3.38

Verwenden Sie die reduce -Funktion,umeine Funktion prod ( lst) zu definieren, die alle in lst befindlichen Zahlen aufmultipliziert.

Aufgabe 3.39

Verwenden Sie die reduce -Funktion, um eine Funktion max ( lst) zu definieren, die das maximale in lst befindliche Element zurückliefert.

Aufgabe 3.40

Verwenden Sie die reduce -Funktion, um eine Liste von Tupeln „flachzuklopfen“ und in eine einfache Liste umzuwandeln. Beispiel: Die Liste

sollte dadurch beispielsweise in die Liste

umgewandelt werden.

3.6Dateien und Verzeichnisse

Python wird mit einer großen Zahl von Standard-Bibliotheken geliefert. Das sind vorprogrammierte Codeteile, die wichtige, häufig benötigte Funktionen bereitstellen. Einige wichtige Bibliotheken, die wir in diesem Abschnitt kennenlernen werden, sind

sys Bietet Zugriff auf Umgebungsvariablen, Standardströme (stdin, stdout, stderr), die Kommandozeile
os Ist die plattformunabhängige 27 Schnittstelle zum Betriebssystem.
os.path Ist ein „Untermodul“ von os und bietet Möglichkeiten zur Bearbeitung von Pfaden.

Ein Modul kann mit

import modulname

geladen werden. Man kann sich alle in einem Modul definierten Funktionsnamen als Stringliste ausgeben lassen mit dem Kommando

dir ( modulname )

Will man etwa auf Parameter zugreifen, die über die Kommandozeile übergeben wurden, die sogenannten Kommandozeilenparameter , so kann man die von Modul sys bereitgestellte Variable argv verwenden. Die Variable argv enthält eine Liste von Strings. Der i -te Eintrag entspricht dem i -ten über die Kommandozeile übergebenen Parameter. Im 0-ten Eintrag, also im String sys.argv [ 0] , ist immer der Name des ausführenden Kommandos gespeichert. Ein Beispiel (das $-Zeichen stellt das Prompt der Unix-Shell Bash dar):

3.6.1Datei-Objekte

Die eingebaute Funktion open

open ( string , modus )

erzeugt ein Dateiobjekt, über das man lesend und/oder schreibend auf Dateien zugreifen kann; diese erwartet zwei String-Argumente: einen Dateinamen und einen Modus ( 'w' für schreibenden Zugriff, 'r' für lesenden Zugriff).

Schreiben in Dateien

Ein Dateiobjekt besitzt die Methode

dateiobj .write( string )

die string in dateiobj schreibt. Beispielsweise erzeugen die folgenden Pythonkommandos eine neue Datei datei.txt, die die beiden mit der write-Methode übergebenen Strings enthält:

Die close-Methode schließt ein Dateiobjekt. Normalerweise ist es jedoch nicht notwendig eine Datei explizit zu schließen, sondern der Python-Interpreter entscheidet im Allgemeinen selbst darüber, wann ein „guter“ Zeitpunkt dafür ist, ein Dateiobjekt aus dem Speicher zu entfernen.

Aufgabe 3.41

Schreiben Sie ein kleines Unix-Shellskript, das genau dasselbe Ergebnis liefert, wie das obige Python-Skript.

Aufgabe 3.42

Schreiben Sie eine Python-Funktion writeLinesToFile, die zwei Argumente übergeben bekommen soll: Einen Dateinamen und eine Liste von Strings; sie soll eine Datei mit dem übergebenen Namen erzeugen und jeden String der Liste in eine extra Zeile dieser Datei schreiben. So soll der Aufruf

die gleiche Wirkung haben wie das Skript in vorigem Beispiel.

Die Methode

dateiobj .writelines( liste )

kann übrigens eine Stringliste in eine Datei schreiben. Beispielsweise kann man mit folgendem Kommando

zwei Zeilen in die Datei test.txt schreiben.

Lesen von Dateien

Die Methode

dateiobj .read()

gibt den kompletten Inhalt einer Datei in einem Python-String zurück. Folgendes Kommando gibt beispielsweise die ersten 11 Zeichen der Datei text.txt zurück:

Aufgabe 3.43

Geben Sie ein Pythonkommando an, mit dem der Inhalt der Datei test.txt ...

(a)rückwärts ausgegeben wird.

(b)zur Hälfte ausgegeben wird.

(c)komplett bis auf das letzte und das vorletzte Zeichen ausgegeben wird.

Die Methode

dateiobj .readlines()

gibt eine Liste von Strings zurück; jeder Eintrag in dieser Liste entspricht einer Zeile von dateiobj . Folgendes Kommando liest beispielsweise die Zeilen der Datei test.txt aus:

Aufgabe 3.44

Was bewirkt das Kommando open ( 'datei.txt' ) .readlines( ) [ :- 2]

Aufgabe 3.45

Schreiben Sie eine Python-Funktion myHead, die einen Dateinamen und eine Zahl n als Argument erhält und die ersten n Zeilen der Datei auf dem Bildschirm ausgibt. Der Aufruf myHead( 'test.txt' ,4) sollte beispielsweise die ersten 4 Zeilen der Datei test.txt auf dem Bildschirm ausgeben.

Aufgabe 3.46

Programmieren Sie das Shell-Kommando head in Python nach und nennen Sie Ihr eigenes Kommando myhead. Das selbst geschriebene myhead soll zwei Optionen kennen:

–-c [-]N : Die ersten N Bytes jeder Datei werden ausgegeben. Falls ein „-“ vorgestellt ist, dann sollen alle Bytes außer den N letzten ausgegeben werden.

–-n [-]N : Die ersten N Zeilen jeder Datei werden ausgegeben. Falls ein „-“ vorgestellt ist, dann sollten alle Zeilen außer den N letzten ausgegeben werden.

Die Verwendung von readlines hat allerdings insbesondere bei großen Dateien einen Nachteil: es liest den gesamten Inhalt einer Datei sofort aus, und dadurch wird viel Speicherplatz verbraucht. Will man das vermeiden, dann kann man den Iterator verwenden, der in jedem Datei-Objekt vorhanden ist. Einen Iterator enthalten auch Listen, Strings, Tupel, Dictionaries usw.; mit ihm kann man nach und nach die Einträge eines Sequenz-Objekts durchlaufen. Der Datei-Iterator liest immer so viele Zeilen aus der Datei aus, wie auch tatsächlich gebraucht werden. Man kann etwa eine for -Schleife verwenden und damit die Zeilen einer Datei einzeln auslesen:

Würde man sich etwa nur für die ersten 1000 Zeilen einer sehr langen Datei interessieren – das ist häufig bei Log-Dateien der Fall – täte man gut daran, die Datei nicht mittels open ( ... ) .readlines() zu öffnen, sondern mittels for die ersten 1000 Zeilen auszulesen.

Aufgabe 3.47

Verwenden Sie die map -Funktion, um genau dasselbe wie mit der gerade eben vorgestellten Listenkomprehension zu implementieren, nämlich die Ausgabe der Liste der großgeschriebenen Zeilen der Datei datei.txt.

Aufgabe 3.48

Geben Sie ein Python-Kommando an, das die Liste der Länge aller Zeilen der Datei test.txt ausgibt.

Aufgabe 3.49

(a)Schreiben Sie eine Python-Funktion, die in der Datei test.txt alle Vorkommen des Wortes eines in das Wort keines umwandelt und dies in die (neu anzulegende) Datei test.txt.2 speichert.

(b)Wir verallgemeinern die Lösung zu Teilaufgabe (a): Schreiben Sie eine Python-Funktion dateiRepl, die drei Argumente übergeben bekommt: einen Dateinamen und zwei Strings. Es sollen dann alle Vorkommen des einen Strings durch den anderen String ersetzt werden, und das Ergebnis in einer neuen Datei mit Endung .2 gespeichert werden. Der Aufruf dateiRepl ( 'test.txt' , 'eines' , 'keines' ) soll also genau den Effekt haben, der in der vorigen Teilaufgabe verlangt wurde.

3.6.2Dateimanipulation mit Listenkomprehensionen

Den Datei-Iterator kann man auch in Listenkomprehensionen verwenden, wie etwa in folgendem Beispiel; hier sollen alle Klein- in Großbuchstaben umwandelt und als Zeilenliste zurückliefert werden.

Mit Listenkomprehensionen und entsprechenden String- und Sequenzfunktionen lassen sich elegant einfach Dateimanipulationen oder Suchoperationen in Dateien ausdrücken. Wir geben im Folgenden einige Beispiele hierfür an.

i) Gib alle Zeilen aus test.txt aus, die weniger als 5 Zeichen enthalten.

Die Schleifenvariable zeile durchläuft nacheinander alle Zeilen der Datei test.txt. Aufgrund der if -Klausel besteht die resultierende Liste aber nur aus den Zeilen, für die len ( zeile) <5 gilt.

Aufgabe 3.50

Verwenden Sie eine Listenkomprehension, um alle Zeilen der Datei test.txt auszugeben, die ...

(a)...mit der Zeichenkette 'Py' beginnen.

(b)...die die Zeichenkette 'Python' enthalten.

(c)...deren letztes Zeichen ein Kleinbuchstabe ist.

ii) Was ist die Länge der längsten Zeile der Datei test.txt?

Die Listenkomprehension erzeugt eine Liste aller Zeilenlängen der Datei test.txt, d. h. eine Integerliste. Die Listenfunktion max liefert den größten Integerwert, also die Länge der längsten Zeile, zurück.

Aufgabe 3.51

Berechnen Sie unter Verwendung einer Listenkomprehension ...

(a)die Summe aller Längen von Zeilen, die mit 'a' beginnen.

(b)die Länge der kürzesten Zeile, die keine Leerzeichen enthält.

iii) Welches ist die längste Zeile der Datei test.txt?

Diese Fragestellung ist der vorigen sehr ähnlich, nur wollen wir hier nicht die Länge, sondern die Zeile selbst zurückgeliefert haben, d. h. wir müssen in der Listenkomprehension beide Informationen strukturiert, etwa in einem Tupel, mitführen: sowohl die Längen der Zeilen (die für die Maximumsbildung benötigt werden) als auch die Zeilen selbst. Dass die Zeilenlänge die erste Komponente der Tupel bildet, die durch die Listenkomprehension erzeugt wird, ist kein Zufall: Sortierungen, Minimums- und Maximumsbildung erfolgen lexikografisch . Ausschlaggebend für diese Sortierungen ist zunächst die erste Komponente einer Struktur. Sollten die ersten Komponenten zweier Objekte identisch sein, so ist die zweite Komponente für deren Sortierung ausschlaggebend, usw. Die „alphabetische“ Sortierung beispielsweise in einem Telefonbuch stellt ein Beispiel einer lexikografischen Sortierung dar: Zuerst wird nach dem ersten Buchstaben gruppiert, dann, sollte der erste Buchstabe zweier Wörter identisch sein, wird nach dem zweiten Buchstaben gruppiert, usw.

Die Anwendung der max -Funktion auf obige Listenkomprehension liefert also dasjenige Tupel aus Zeile und zugehöriger Zeilenlänge zurück, das die maximale Länge hat. Um die Zeile selbst zu erhalten, müssen wir mittels des Indexoperators seq [ 1] nur noch die zweite Komponente dieses Tupels extrahieren.

Wir haben gerade eben ein häufig verwendetes Muster kennengelernt, das in der Literatur auch als „Decor-Undecor-Idiom“ bezeichnet wird: Listenwerte werden mit Informationen angereichert, d. h. „dekoriert“, um sie nach bestimmten Kriterien zu sortieren. Anschließend wird diese Dekoration wieder entfernt („undecor“), was in obigem Beispiel mittels des Indexoperators seq [ 1] erfolgt.

Aufgabe 3.52

Verwenden Sie eine Listenkomprehension, umdiejenige Zeile der Datei ’ test.txt’ auszugeben, die ...

(a)...am häufigsten das Zeichen 'a' enthält.

(b)...die meisten Wörter enthält.

(c)...das längste Wort enthält.

iv) Wie viele Leerzeilen enthält die Datei test.txt?

Die erzeugte Liste enthält nur diejenigen Zeilen aus der Datei test.txt, die genau ein Zeichen enthalten, d. h. für die len ( zeile) ==1 gilt. Dies sind genau die Leerzeilen, denn diese bestehen lediglich aus dem Newline-Zeichen ' \n' . Umzu wissen, wie viele Leerzeilen gefunden wurden, müssen die Einträge in der Listenkomprehension gezählt werden. Dies erledigt die Anwendung der len -Funktion.

v) Welche in der Datei test.txt enthaltenen Wörter beginnen mit 'a' .

Dies lässt sich am besten über eine Listenkomprehension lösen, die zwei geschachtelte Schleifen verwendet. Die äußere Schleife durchläuft die Zeilen der Datei test.txt . Für jede einzelne Zeile durchläuft die Variable w der inneren Schleife die Liste der Wör ter dieser Zeile, erzeugt durch den Ausdruck zeile.split () . Durch die if -Anweisung werden nur diejenigen Wörter „durchgelassen“, die mit 'a' beginnen.

Aufgabe 3.53

Was ist der „Sinn“ des folgenden Statements?

3.6.3Verzeichnisse

Eine häufige Aufgabe besteht darin, eine bestimmte Operation auf eine große Menge von Dateien in einem bestimmten Verzeichnis oder in einem ganzen Verzeichnisbaum anzuwenden. Möglicherweise wollen Sie bestimmte Ersetzungen in allen *.tex-Dateien in einem bestimmten Verzeichnis vornehmen oder nach bestimmten Java-Schlüsselwörtern in allen Java-Dateien auf Ihrem Home-Verzeichnisbaum suchen.

Manche dieser Aufgaben sind vielleicht in der Bash einfacher zu realisieren, aber in Python können Sie diese Aufgaben plattformunabhängig programmieren 28 . Außerdem stehen Ihnen unter Python viel mächtigere Möglichkeiten, wie strukturierte Datentypen, Iteratoren oder Listenkomprehensionen zur Verfügung als unter der Bash. Je komplexer Ihre Aufgabe ist, die Sie realisieren wollen, desto empfehlenswerter ist die einer höheren getypten Programmiersprache wie Python anstelle der Verwendung eines Bash-Skripts.

Durch die Einträge eines Verzeichnisses iterieren

Der Aufruf

os.listdir ( dir )

liefert eine String-Liste der Namen aller Einträge im Verzeichnis dir zurück. Ein Beispiel:

Aufgabe 3.54

Geben Sie einen Python-Einzeiler an, mit dem Sie ...

(a)sich die Anzahl der Einträge im aktuellen Verzeichnis ausgeben lassen.

(b)sich die Längen aller Dateinamen im aktuellen Verzeichnis ausgeben lassen.

(c)sich die Anzahl der Dateien im aktuellen Verzeichnis mit Endung . c und mit Endung .py ausgeben lassen.

(d)sich alle Dateien mit Endung .txt aus dem aktuellen Verzeichnis ausgeben lassen.

(e)sich die Anzahl der Zeilen aller Dateien mit Endung .txt ausgeben lassen.

Durch die Einträge eines Verzeichnisbaums iterieren

Der Aufruf

os.walk ( dir )

liefert einen Iterator über den Verzeichnisbaum unterhalb des Verzeichnisses dir zurück. Jeder Aufruf dieses Iterators liefert ein 3-Tupel der folgenden Form zurück:

( dirpath , dirnames , filenames )

Dabei ist dirpath ein String, der den Pfad zum momentan besuchten Verzeichnis enthält, dirnames ist eine Liste von Strings, die die Namen der Unterverzeichnisse des momentan besuchten Verzeichnisses enthält und filenames ist eine Liste von Strings, die die Namen der Dateien, die keine Verzeichnisse sind, im momentan besuchten Verzeichnis enthält.

Folgendes Programmbeispiel findet alle Dateien mit Endung .tex, in denen sich der String python befindet, im Verzeichnisbaum unterhalb ' .' :

Einige Erläuterungen hierzu: verzName ist immer der Name des Verzeichnisses, das gerade angelaufen wurde; verzeichnisse ist die Liste der Unterverzeichnisse (für die wir uns in diesem Fall nicht interessieren) und dateien ist die Liste aller Datei en im Verzeichnis, das gerade angelaufen wurde. Mit der String-Methode endswith wird geprüft, ob der Name der entsprechenden Datei mit .tex endet. Mit der Funktion os.path.joinwerden der Pfadname und der Dateiname zusammengehängt 29 . Diese vollständigen Pfadnamen benötigen wir für die Funktion open , die den gesamten Inhalt der Datei in einen String einliest; mit der Operation in (siehe Abschnitt 3.2.5 ) können wir testen, ob dieser den String 'python' enthält.

Mit noch weniger Codezeilen kommt man aus, wenn man für diese Aufgabe eine Listenkomprehension verwendet:

Aufgabe 3.55

(a)Schreiben Sie ein Python-Skript, das Ihnen die Datei mit dem längsten Namen (unter allen Dateien in Ihrem kompletten Homeverzeichnis) zurückliefert.

(b)Schreiben Sie ein Python-Skript, das Ihnen diejenige .py-Datei (unter alle .py-Dateien in Ihrem kompletten Home-Verzeichnisbaum) zurückliefert, die die längste Zeile besitzt.

Aufgabe 3.56

Schreiben Sie eine Funktion, die zählt, wie oft das Schlüsselwort for in allen Python-Dateien in Ihrem kompletten Homeverzeichnisbaum vorkommt.

3.7Objektorientierte Programmierung

Zentral für die objektorientierte Programmierung ist die Möglichkeit neue Klassen erzeugen zu können. Eine Klasse ist eigentlich nichts anderes ist als ein Python-Typ, genau wie int , str , list oder dict . Ein bestimmter Wert einer Klasse wird im Sprachjargon der Objektorientierten Programmierung auch als Objekt bezeichnet. Die Aussage

[ 2,4,6] ist ein Wert vom Typ list

würde man im Sprachjargon der Objektorientieren Programmieren dagegen eher als

[ 2,4,6] ist ein Objekt der Klasse list

formulieren.

Die Kommandos, die eine Klasse anbietet, um ihre Objekte zu manipulieren oder Berechnungen auf ihnen auszuführen, werden im Sprachjargon der Objektorientierten Programmierung als Methoden bezeichnet. Der Aufruf eines Kommandos f, das Berechnungen über ein Objekt obj durchführt und zusätzliche Argument arg 1 , arg 2 , ...bekommt, wird im Zusammenhang mit der Objektorientierten Programmieren nicht als f ( obj , arg 1 , arg 2 , ...) geschrieben, sondern in der Syntax

obj .f ( arg 1 , arg 2 , ...)

geschrieben.

Kommandos wie beispielsweise list .append, dict .keys oder str .endswidth werden wir in diesem Kapitel also grundsätzlich als Methoden bezeichnen.

3.7.1Definition und Verwendung einer Klasse

Zunächst ist es notwendig, dass die Leser ein grobe Vorstellung davon bekommen, was eine Klasse ist und wie sie verwendet wird. Grob gesprochen, ist eine Klasse eine Gruppierung von Daten und dazugehörigen Funktionen, die in diesem Zusammenhang Methoden genannt werden. Die in einer Klasse enthaltenen „Dinge“ sollten konzeptuell zusammengehören. Häufig beziehen sich Klassen auf Dinge der realen Welt, wie etwa Kunden, Personen, Autos, graphische Objekte oder andere real vorhandenen Dinge, die in der Software repräsentiert sein sollen.

Die Syntax zur Erzeugung einer neuen Klasse lautet:

Die Kommandos werden bei Erzeugung der Klasse alle ausgeführt. Typischerweie sind diese Kommandos Methoden-Definitionen oder Variablen-Zuweisungen. Die Variablen, denen innerhalb einer Klasse ein Wert zugewiesen wird, werden üblicherweise als Attribute bezeichnet. Listing 3.41 zeigt ein Beispiel einer einfachen Klassendefinition:

Listing 3.41. Definition einer einfachen Klasse

In Zeile 2 wird eine Variable typ definiert und in Zeile 3 eine Methode sagHallo. Eine solche Variable heißt auch Attribut oder Klassenvariable. Alle Methoden haben Zugriff auf diese Klassenvariable über die Referenz Auto.typ. Jede Methode muss als erstes Argument die Variable „ self “ übergeben bekommen. Die Variable self enthält immer die Referenz auf das Objekt selbst. Dieses wird bei jedem Methodenaufruf implizit mit übergeben. Betrachten wir beispielsweise den Methodenaufruf lst.append( 3) : In diesem Fall ist self nichts anderes als das Listenobjekt lst.

Durch die Zuweisung

kann man eine Instanz der Klasse erzeugen, d. h. eine Variable vom Typ Auto, im OO-Sprachjargon als ein Objekt bezeichnet, in diesem Fall ein Objekt der Klasse Auto. Auf die Methode sagHallo kann man mittels einAuto.sagHallo zugreifen:

Enthält eine Klassendefinition die Methode __init__, so wird diese Methode bei jedem Erzeugen eines Objektes automatisch ausgeführt. Neben dem obligaten Argument self kann die __init__-Methode noch weitere Argumente enthalten, und die Erzeugung von Objekten kann so abhängig von bestimmten Parametern erfolgen. Listing 3.42 zeigt eine modifizierte Definition der Klasse Auto:

Listing 3.42. Definition einer komplexeren Auto-Klasse

Bei der Erzeugung einer neuen Instanz von Auto wird nun immer automatisch die __init__-Methode ausgeführt, die neben self zwei weitere Argumente erwartet, die dann in Zeile 6 und 7 den (Objekt-)Attributen typ und farbe zugewiesen werden. Man sieht, dass man mittels self .typ bzw. self .farbe auf die Attribute typ bzw. farbe des aktuellen Objektes zugreifen kann.

Die Attribute self .typ und self .farbe gehören also zu einem bestimmten Objekt der Klasse Auto und können für unterschiedliche Objekte unterschiedliche Werte annehmen. Dagegen ist das in Zeile 2 definierte Attribut anzAutos ein Klassenattribut, d. h. es gehört nicht zu einer bestimmten Instanz von Auto, sondern ist global für alle Objekte der Klasse sichtbar. Gleiches gilt für alle Methodendeklarationen – auch sie sind global für alle Objekte der Klasse sichtbar.

Bei jeder Erzeugung einer Klasseninstanz erhöhen wir die Variable anzAutos um Eins. Die in Zeile 10 definierte spezielle Methode __del__ wird immer dann automatisch aufgerufen, wenn mittels des del -Kommandos ein Objekt der Klasse gelöscht wird; in Zeile 11 erniedrigen wir die Variable anzAutos um Eins, wenn ein Objekt gelöscht wird.

Wir erzeugen in den Variablen a1, a2 und a3 drei verschiedene Variablen vom Typ Auto:

und können uns nun mittels der Methode ueberDich Informationen über das jeweilige Objekt ausgeben lassen:

Man kann auch eine neue Klasse erzeugen, die auf den Attributen und Methoden einer anderen Klasse basiert – im Jargon der Objektorientierten Programmierung nennt man das auch Vererbung . Es ist grundsätzlich so, dass eine Klasse, die ein spezielleres Konzept modelliert, immer von der Klasse erbt, die ein allgemeineres Konzept repräsentiert. Stellen wir uns vor, wir wollten die drei Klassen Hochschulangehoeriger, Professor und InformatikProfessor definieren. Dann wäre es sinnvoll, die Klasse Professor von der Klasse Hochschulangehoeriger erben zu lassen und die Klasse InformatikProfessor von der Klasse Professor erben zu lassen. Oder anknüpfend an unser obiges Beispiel mit der Klasse Auto, könnte man sich vorstellen, eine neue Klasse Oldtimer zu definieren, die von der Klasse Auto erbt. Das Alter eines Autos beispielsweise könnte uns nur dann interessieren, wenn es sich um einen Oldtimer handelt. Diese Tatsache kann man wie folgt in Python realisieren:

Wie man sieht, muss gleich bei der Deklaration der Klasse die Klasse, von der geerbt wird (hier: Auto) in Klammern mit angegeben werden. Ebenfalls zu sehen ist, dass man die __init__-Methode der Basisklasse explizit aufrufen muss. Gleiches gilt auch für andere gleichlautende Methoden: Die Methode ueberDich muss die gleichlautende Methode der Basisklasse explizit aufrufen. Wir können nun ein Objekt vom Typ Oldtimer folgendermaßen erzeugen und verwenden:

Neben der __init__-Methode und der __del__-Methode gibt es in Python noch eine Reihe weiterer Methoden mit spezieller Bedeutung, unter anderem:

–__str__( self ) : Diese Methode berechnet die String-Repräsentation eines bestimmten Objektes; sie wird durch Pythons interne Funktion str ( ... ) und durch die print -Funktion aufgerufen.

–__cmp__( self ,x) : Diese Methode wird bei Verwendung von Vergleichsoperationen aufgerufen; sie sollte eine negative ganze Zahl zurückliefern, falls self <x; sie sollte 0 zurückliefern, falls self ==x; sie sollte eine positive ganze Zahl zurückliefern, falls self >x.

–__getitem__( self ,i) :Wird bei der Auswertung des Ausdrucks self [ i] ausgeführt.

–__setitem__( self ,i,v) : Wird bei einer Zuweisung self [ i] =v ausgeführt.

–__len__( self ) :Wird bei der Ausführung der Python internen Funktion len ( ... ) aufgerufen.

Aufgabe 3.57

Erstellen Sie eine Python-Klasse Auto. Die Klasse soll die Methoden fahre, kaputt und repariere haben.

Nach dreimaligem Fahren (d. h. nach dreimaligem Aufruf der Methode fahre) geht ein Auto kaputt. Danach kann man es nicht mehr fahren. Nach Aufruf von repariere ist das Auto repariert und es kann wieder gefahren werden, nach dreimal ist es wieder kaputt, usw. Die Methode kaputt liefert True oder False zurück, je nachdem, ob das Auto kaputt ist oder nicht. Beispiel:

Programmieren Sie in Python die Klasse Auto, die sich wie oben beschrieben verhält – inklusive der drei Methoden fahre, repariere und kaputt.

Aufgabe 3.58

Erstellen Sie eine Pythonklasse Stack. Die Stack-Datenstruktur, auch Stapel-Datenstruktur genannt, bietet eine Sammlung von Elementen an, die logisch in Form eines Stapels (von Elementen, Aufgaben, usw.) angeordnet sind. Eingefügt werden kann ein neues Element immer nur oben auf dem Stapel. Das Einfügen bezeichnet man in diesem Zusammenhang auch als Push-Operation. Ausgelesen werden kann ein Element ebenfalls nur von oben. Das Auslesen eines Elements und anschließendes Löschen bezeichnet man im Zusammenhang mit der Stapel-Datenstruktur als Pop-Operation.

Nachfolgendes Beispiel soll die Funktionsweise der Klasse Stack veranschaulichen: