Featured Artikel

Einen Netzplan erstellen

In diesem Beitrag stelle ich ein kleines Python-Programm vor, mit dem sich ein Netzplan „durchrechnen“ lässt. Netzpläne sind ein beliebtes Planungswerkzeug im Bereich des Projektmanagement. Es gibt „jede Menge“ Beschreibungen und Beispiele im Internet. Eine Webseite, auf der das Erstellen eines Netzplans besonders anschaulich beschrieben wird (so, dass sogar ich es verstanden habe;) ist

*** Der Beitragt ist aktuell nur ein ENTWURF – es fehlen noch einige Erläuterungen und der Text enthält sicher noch einige Rechtschreibfehler usw. das Programm NetzplanV1.py funktioniert aber ***

Netzpläne in Theorie und Praxis in drei Folgen

Eine weitere, etwas kompaktere, aber ebenfalls hervorragend illustrierte Erklärung gibt es unter der folgenden Adresse:

Netzplantechnik: Eine Schritt-für-Schritt-Anleitung mit Beispiel

Auch wenn das Thema Netzplantechnik nicht nur unter den genannten Webseiten, sondern an vielen anderen Stellen im Web und mit Sicherheit in jedem Fachbuch zum Thema Projektmanagement beschrieben wird, ein paar Stichworte von mir, damit ihr das folgende Python-Programm besser verstehen könnt.

Der Sinn und Zweck eines Netzplan ist es, die Projektdauer eines aus mehreren Vorgängen bestehenden Projekts besser beurteilen zu können und zu erkennen, welche Vorgänge einen zeitlichen Puffer besitzen und welche Vorgänge „kritisch“ sind, da sie keinen Puffer besitzen.

Ein Netzplan enthält alle Vorgänge (also abgeschlossene Teilaufgaben) eines Projekts. Jeder Vorgang wird durch einen Kasten dargestellt.

Die Kästen werden durch Pfeile verbunden. Ein Vorgang kann beliebig viele Vorgänger und Nachfolger besitzen. Ausnahmen sind natürlich die Vorgänge am Anfang und am Ende, die keine Vorgänger bzw. Nachfolger besitzen.

Jeder Vorgangskasten enthält genau acht Angaben:

>Eine Bezeichnung oder nur eine Nummer oder einen Buchstaben

>Eine Dauer in einer Zeiteinheit

>Einen frühesten Anfangszeitpunkt (FAZ)

>EInen frühesten Endzeitpunkt (FEZ)

>Einen spätesten Anfangszeitpunkt (SAZ)

>Einen spätesten Endzeitpunkt (SEZ)

>Einen Gesamtpuffer (GP)

>Einen freien Puffer (FP)

GP und FP sind ebenfalls Zeiteinheiten.

Die wichtigste Aufgabe eines Netzplans ist es, die Pufferzeiten zu erhalten, da sich anhand dieser Zahlen erkennen lässt, wo es Spielraum für Verzögerungen gibt, ohne dass dadurch der Fertigstellungszeitpunkt verschoben werden muss.

Eine weitere wichtige Kenngröße ist der kritische Pfad. Er umfasst alle Vorgänge, die keinen Zeitpuffer aufweisen, bei denen es also keinen Spielraum für eine Verschiebung gibt.

EIn Netzplan wird in vier Schritten erstellt:

Schritt 1: Aufschreiben aller Vorgänge als Kästen und Eintragen der Verbindungspfeile und der Projektdauer.

Dieser Schritt ist sehr einfach und bedarf keiner Erklärungen. Eventuell muss man die einzelnen Vorgänge und ihre Beziehung zueinander aus einer Aufgabenbeschreibung „herauslesen“ (falls man das Fach „Einführung in das Projektmanagement“ aus irgendeinem Grund absolvieren muss).

Schritt 2: Eintragen der Werte für FAZ und FEZ.

Das sog. Vorwärtsrechnen. In einer rein linearen Folge von Vorgängen beginnt der FAZ des ersten Vorgangs (bzw. allgemein dem oder die Vorgänge ohne einen Vorgänger) bei 0, der FEZ ist immer FAZ + Dauer usw.

Werden zwei oder mehr Schritte parallel ausgeführt, ist der FAZ des Teilschrittes, der die parallel ausgeführten Schritte wieder zusammenführt, der größte FEZ-Wert aller Teilschritte.

Schritt 3: Eintragen der Werte für SAZ und SEZ.

Das sog. Rückwärtsrechnen, das immer mit dem letzten Vorgang beginnt. Konkret, es beginnt mit dem letzten Vorgang ohne einen Nachfolger (davon kann es auch mehrere geben). Beim „ersten“ Vorgang (in der umgekehrten Reihenfolge betrachtet), sind FEZ und SEZ identisch. Der SAZ-Wert ergibt sich aus der Differenz von SEZ und Dauer. Bei allen anderen Teilschritten entspricht der SEZ-Wert dem SAZ-Wert des „Vorgängers“ (immer bezogen auf die umgekehrte Reihenfolge). Gibt es mehrere Vorgänger, wird der kleinste Wert genommen.

Schritt 4: Berechnen des Gesamtpuffers (GP) für jeden Vorgang

Sind die SAZ-Werte bekannt, kann der Gesamtpuffer jedes Vorgangs aus der Differenz GP = SAZ – FAZ berechnet werden. Der freie Puffer FP ergibt sich aus der Differenz des FAZ-Wertes des Nachfolgers und des FAZ-Wertes. Gibt es mehrere FAZ-Werte, wird der kleinste Wert genommen.

Wer jetzt noch dabei ist, Respekt, den wer nicht gerade Projektmanagement im Rahmen einer Ausbildung lernen muss, wird das Thema maximal am Rande interessieren.

Der beschriebene Ablauf „schreitt gerade zu nach einer Umsetzung in der Programmiersprache Deiner Wahl. In diesem Blog ist das natürlich Python.

Gleich vorweg. Das Python-Programm, das im Folgenden vorgestellt wird, ist eine schlichte Konsolenanwendung. Die Daten des Netzplanes werden aus einer Textdatei eingelesen und die Werte werden nur in der Konsole ausgegeben. Viel schöner wäre es natürlich, wenn man richtige Kästen auf einer Fläche platzieren und die Eckdaten einfach eintragen könnte. So etwas zu programmieren ist natürlich deutlich aufwändiger. Ich würde eine solche Anwendung als Webapp oder Smartphone-App umsetzen (und dann nicht unbedingt Python verwenden). Oder gleich den Netzplan auf draw.io zeichnen, das Ganze ausdrucken und die Zahlen mit einem Stift eintragen.

Das Python-Programm, das im Folgenden vorgestellt wird, ist in erster Linie eine hervorragende „Programmierer-Challenge“, eine sehr gute Übung und ein schönes Anschauungsbeispiel für eine Programmiersprache, bei der das Programmieren einfach Spaß macht.

Das Thema Dateizugriff kommt vor, es ist eine Klasse im Spiel, Objekte werden in einem Dictionary abgelegt und u.a. kommt auch eine Prise List Comprehension ins Spiel. Also alles das, was ein Python-Programm ausmacht. Das Programm kann sowohl als Modul als auch als reguläres Programm verwendet werden.

Umgesetzt habe ich alles mit Visual Studio Code, mit dem ich gerne arbeite. Jeder verwendet natürlich den Editor oder die IDE seiner Wahl.

Wer sich das Abtippen ersparen möchte, findet die Programmdatei in meinem GitHup-Repo zusammen mit Beispieldateien für Netzpläne und einem weiteren Python-Programm, welches das Netzplan-Programm als Modul einbindet und alle Txt-Dateien im aktuellen Verzeichnis der Reihe nach umsetzt.

https://github.com/pemo11/pynetzplan

Dazu der übliche „Disclaimer“. Das kleine Programm versteht sich in erster Linie als Übungsbeispiel für das Erlernen der Python-Programmierung und der Netzplantechnik im Allgemeinen (z.B. für den Fall, dass Thema in einer Prüfung ein Thema ist). Es ist (natürlich) nicht für den produktiven Einsatz gedacht.

Schritt 1: Die Formalismen

Ein Python-Programm beginnt in der Regel mit einer Kommentarzeile und diversen import-Befehlen. Die unter Linux übliche „shebang“-Zeile kann unter Windows entfallen. Persönlich halte ich diese Zeile auch unter Linux für entbehrlich, da man Python-Programme auch unter Linux über den Python-Interpreter startet (auf der anderen Seite habe ich, wenn ich ehrlich bin, von Linux nicht wirklich eine Ahnung;).

#!/usr/bin/env python3
# Pufferzeiten bei einem Netzplan berechnen
import os
import sys

Schritt 2: Die Klasse Vorgang

Jeder Vorgang wird durch die Klasse Vorgang repräsentiert, die sich in erster Linie durch ihre Atribute auszeichnet. Sie spielt später aber nur beim Einlesen der Textdatei eine Rolle.

# Repräsentiert einen Vorgang
class Vorgang:

    def __init__(self, Name, Beschreibung, Dauer, Vorgaenger, Nachfolger):
        self.Name = Name
        self.Beschreibung = Beschreibung
        self.Dauer = int(Dauer)
        self.Vorgaenger = Vorgaenger
        self.AnzahlVorgaenger = 0 if Vorgaenger[0] == "" else len(Vorgaenger)
        self.Nachfolger = Nachfolger
        self.AnzahlNachfolger = 0 if Nachfolger[0] == "" else len(Nachfolger)
        self.FAZ = 0
        self.SAZ = 0
        self.FEZ = 0
        self.SEZ = 0
        self.GP = 0
        self.FP = 0

    def __toString__(self):
        if len(self.Beschreibung) == 0:
            return f"{self.Name}: Dauer: { self.Dauer} FAZ/FEZ: {self.FAZ}/{self.FEZ} SAZ/SEZ: {self.SAZ}/{self.SEZ} GP/FP: {self.GP}/{self.FP}"
        else:
            return f"{self.Beschreibung}: Dauer: { self.Dauer} FAZ/FEZ: {self.FAZ}/{self.FEZ} SAZ/SEZ: {self.SAZ}/{self.SEZ} GP/FP: {self.GP}/{self.FP}"


Schritt 3: Die Function ErstelleNetzplan

Da die Programmdatei Netzplan.py auch als Modul in ein anderes Programm einbindbar sein soll, enthält sie eine Function mit dem Namen ErstelleNetzplan, die im weiteren Verlauf des Programms immer dann automatisch ausgeführt wird, wenn das Programm direkt gestartet wurde. Die Function ist relativ umfangreich. Ihr wird der Pfad einer Textdatei übergeben, in der alle Aufgaben definiert sind (mehr dazu in Kürze).

Ich werde sie in naher Zukunft ausführlicher beschreiben, da sie das „Herzstück“ des kleinen Programms ist.

def ErstelleNetzplan(Dateipfad):

    print(f"*** Verarbeite {Dateipfad} ***")

    netzplan = {}

    with open(Dateipfad, encoding="Utf-8") as fh:
        for zeile in fh:
            if zeile == "":
                break
            if zeile[-1] == "\n":
                zeile = zeile[:-1]
            name, beschreibung, dauer, vor, nach =  zeile.split(",")
            # Es soll immer eine Liste der Vorgänger und Nachfolger gebildet werden
            vor = vor.split(":")
            nach = nach.split(":")
            netzplan[name] = Vorgang(name,beschreibung,dauer,vor,nach)

    # Schritt 1: FAZ und FEZ im Vorwärtsgang berechnen
    for pk in netzplan:
        p = netzplan[pk]
        # Gibt es mehrere Vorgänger?
        if p.AnzahlVorgaenger == 1:
            v = netzplan[p.Vorgaenger[0]]
            p.FAZ = v.FEZ
        elif p.AnzahlVorgaenger > 1:
            maxFEZ = max([netzplan[pv].FEZ for pv in p.Vorgaenger])
            p.FAZ = maxFEZ
        else:
            p.FAZ = 0
            p.FEZ = p.FAZ + p.Dauer

    # Schritt 2: SAZ und SEZ im Rückwärtsgang berechnen
    # Und bei der Gelegenheit auch GP und FP
    pKeys = list(netzplan.keys())
    pKeys.reverse()

    for pk in pKeys:
        p = netzplan[pk]
        # if len(p.Nachfolger) == 0:
        if p.AnzahlNachfolger == 0:
            # Weitere Randbedingung - es kann mehrere Vorgänger ohne Nachfolger geben,
            # es muss daher der größte FEZ genommen werden
            maxFEZ = max([netzplan[pn].FEZ for pn in pKeys if netzplan[pn].AnzahlNachfolger == 0])
            p.SEZ = maxFEZ
        elif p.AnzahlNachfolger == 1:
            nv = netzplan[p.Nachfolger[0]]
            p.SEZ = nv.SAZ
            p.FP = nv.FAZ - p.FEZ
        else:
            minSAZ = min([netzplan[pn].SAZ for pn in p.Nachfolger])
            minFAZ = min([netzplan[pn].FAZ for pn in p.Nachfolger])
            p.SEZ = minSAZ
            p.FP = minFAZ - p.FEZ
            p.SAZ = p.SEZ - p.Dauer
            p.GP = p.SAZ - p.FAZ

    for p in netzplan.values():
        print(p.__toString__())

    # Schritt 3: Kritischen Pfad ermitteln
    # Also alle Vorgänge, bei denen GP und FP 0 sind
    kritischerPfad = []
    # Das erste Element holen
    # Weitere Randbedingung  - gibt es mehrere Vorgänge ohne Vorgänger, hole den mit GP=0
    # p = list(netzplan.values())[0]
    p = [a for a in netzplan.values() if a.AnzahlVorgaenger == 0][0]
    kritischerPfad.append(p.Beschreibung if len(p.Beschreibung) > 0 else p.Name)
    while p.AnzahlNachfolger > 0:
        # Gibt es einen Nachfolger und sind GP und FP gleich 0?
        if p.AnzahlNachfolger == 1 and p.GP == 0 and p.FP == 0:
            p = netzplan[p.Nachfolger[0]]
            kritischerPfad.append(p.Beschreibung if len(p.Beschreibung) > 0 else p.Name)
        else:
            l = [netzplan[np] for np in p.Nachfolger if netzplan[np].GP == 0 and netzplan[np].FP == 0]
            if len(l) == 0:
                print("*** Es kann kein kritischer Pfad gebildet werden! ***")
                break
            p = l[0]
            kritischerPfad.append(p.Beschreibung if len(p.Beschreibung) > 0 else p.Name)
 
    if len(kritischerPfad) > 1:
        print("Der kritische Pfad:",end=" ")
        print(",".join(kritischerPfad))

Schritt 4: Das Finale

Das „Finale“ des Programms besteht in der üblichen Abfrage der Variablen __Name__ auf den Wert „__Main__„. Besitzt diese Variable diesen Wert bedeutet es, dass das Python-Programm direkt ausgeführt wurde. In diesem Fall wird die Function ErstelleNetzplan mit dem Argument, das beim Aufruf des Programms übergeben wurde, als Parameterwert.

# Prüfen, ob Datei als Modul ausgeführt wird
if __name__ == "__main__":
    if len(sys.argv) == 1:
        print("!!! Aufruf: Netzplan.py Dateiname")
        exit(1)
    dateiPfad = sys.argv[1]
    # nur bei VS Code erforderlich
    dateiPfad = os.path.join(os.path.dirname(__file__), dateiPfad)
    ErstelleNetzplan(dateiPfad)

Der Aufruf für einen ersten Test

Bliebe noch zu klären. wie das kleine Programm aufgerufen wird. Ausgangspunkt ist eine Textdatei mit einer Definition aller Aufgaben. Jede Aufgabe wird durch eine Zeile definiert. Jede Zeile enthält genau 5 Angaben:

>Ein Name (Zahl oder Buchstabe)

>Eine optionale Beschreibung des Vorgangs

>Die Dauer des Vorgangs

>Die Namen der Vorgänger. Gibt es mehrere Vorgänger, werden diese durch einen Doppelpunkt getrennt

>Die Namen der Nachfolger. Gibt es mehrere Nachfolger, werden diese durch einen Doppelpunkt getrennt

Gibt es keine Vorgänger oder Nachfolger, bleibt die Spalte einfach leer.

Eine Zeile kann damit wie folgt aufgebaut sein:

1,Online-Umfrage,12,,3

oder

1,,4,,2

Die vollständige Definitionsdatei für einen Netzplan kann daher wie folgt aussehen:

1,Online-Umfrage,12,,3
2,Experteninterview,8,,3
3,Auswertung,4,1:2,5:6
4,Kostenplan aufstellen,10,,6:7
5,Prototyp-Ausschreibung,8,3,8
6,Konzept erstellen,6,3:4,8
7,Finanzierungsplan,10,4,10
8,Prototyp entwickeln,12,5:6,9
9,Test mit Bürgern,4,8,10:11
10,Werbematerial erstellen,4,7:9,
11,Beschlussvorlage erstellen,6,9,

Befindet sich der Netzplan in der Datei Np01.txt, sieht der Aufruf des Python-Programms wie folgt aus:

python netzplan.py Np01.txt

Anschließend werden zu jedem Vorgang die Werte für FAZ/SAZ, FEZ/SEZ und GP/FP in der Konsole ausgegeben.

Natürlich wäre es schön, wenn ein richtiger Netzplan z.B. als Bitmap entstehen würde. Theoretisch wäre der Aufwand nicht allzu groß, wenn der Output in SVG konvertiert werden würde. Und wie erzeugt man SVG-XML per Python? Zum Beispiel mit dem Modul SVGWrite. Wenn ich in den nächsten Wochen einmal viel Zeit habe, wäre das das nächste Python-Projekt.

Das Python-Ökosystem im Überblick (Stand: Juli 2020)

In diesem Beitrag geht es zur Abwechslung nicht um die Programmierung mit Python. Ich möchte das umfangreiche „Python-Ökosystem“ mit wenigen Worten vorstellen. Wer sich zum ersten Mal mit Python beschäftigt, wird mit vielen Namen konfrontiert, die alle etwas mit Python zu tun haben, aber doch etwas Eigenes sind. Da gibt es u.a. CPython, Jupyter, Anconda, IronPython, PyPy oder Django. Und dabei habe ich die „Hunderttausend“ Erweiterungen, die es für Python gibt, sowie die zahlreichen IDEs mit ihren teilweise ausgefallenen Namen gar nicht aufgezählt. Bis auf CPython sind alle Begriffe eine „Spielart“ von Python, die man installieren und mit der „man“ arbeiten kann.

Wer die Programmierung mit Python erlernen möchte, sollte Python für sein Betriebssystem in der aktuellsten Version von Python.org installieren und sich um die anderen Vertreter zunächst keine Gedanken machen. Als IDE (also als „Programmiereditor“) ist Idle sehr gut geeignet, das mit Python in der Regel installiert wird. Ansonsten empfehle ich Visual Studio Code als universellen Programmiereditor – hier muss die Python-Erweiterung von Microsoft aber einmalig nachinstalliert werden, was weniger als eine Minute in Anspruch nimmt. Alles kostenlos und für alle Plattformen verfügbar.

Der Reihe nach und alphabetisch.

Anaconda

Anaconda ist der Name einer Python-Distribution, die in den Datenwissenschaften eingesetzt wird. Nach eigener Aussage ist Anaconda die beliebteste Plattform für Datenanalye. Ihr Vorteil ist, dass alle für das Analysieren von Daten benötigten Packages von Anfang dabei sind (aktuell > 250). Auch wenn hinter Anaconda mit der Anaconda Inc. aus Austin/Texas in den USA ein kommerzieller Anbieter steht, ist Anaconda kostenlos.

https://de.wikipedia.org/wiki/Anaconda_(Python-Distribution)

CPython

CPython ist die Referenzimplementierung der Python-Foundation, in der alle Neuerungen implementiert werden. Wer eine eigene Python-Distribution herausgegeben möchte, sollte sich an CPython orientieren. Für den Anwender spielt CPython insofern eine Rolle, da die von der Python Foundation herausgegebene Python-Version auf diese Referenz-Implementierung basiert.

CPython ist das „Standard-Python“.

IronPython

IronPython ist ein bei Microsoft bereits im Jahr 2006 (!) gestartetes Open Source-Projekt. Es ist aktuell zu CPyton 2.7 kompatibel. Während Python eine eigene Laufzeitumgebung mitbringt, setzt IronPython auf dem .Net Framework, so dass die gesamte .Net-Klassenbibliothek zur Verfügung steht. Das ist vor allem für Entwickler, die ohnehin .Net verwenden, oder deren Programme nur unter WIndows ausführen sollen, attraktiv. Programmiert wird mit Microsoft Visual Studio, (nicht zu verwechseln mit Visual Studio Code), das in der Community Edition seit vielen Jahren kostenlos ist.

Nachdem der geistige Vater des Projekts, Jim Hugunin, Microsoft vor ein paar Jahren wieder verlassen hatte, schlief das Projekt offenbar etwas ein, wenngleich es laut Projektportal fortgeführt werden soll. Aktuell wird nur Python in der Version 2.7 unterstützt. Ob es eine Unterstützung für 3.x und eine Portierung auf .NET Core geben wird, so dass IronPython auch unter anderem Plattformen eingesetzt werden kann, hängt offenbar von der Frage ab, wer bereit ist an dem Projekt mitzuarbeiten.

https://de.wikipedia.org/wiki/IronPython

Jupyter

Die wichtigse Kleinigkeit vorweg. Es heißt „Jupyter“ und nicht „Jupiter“, da die Programmierumgebung zuerst nur für die drei Programmiersprachen Julia, Python und R umgesetzt wurde. Inzwischen werden viele weitere Sprachen als Kernel“ angeboten, so dass es sich um eine vielseitige Angelegenheit handelt. Ausgesprochen wird der Name aber wie der Planet bzw. es spielt auch keine ganz so große Rolle;)

Alles begann im Jahr 2014 mit Projekt IPython. IPython existiert aber weiter als Python-Implementierung und „Kernel“ für Jupyter und kann noch unabhängig von Jupyter installiert werden.

https://de.wikipedia.org/wiki/Project_Jupyter

Bei Jupyter arbeiten die User mit einem sog. Notebook, also einem Notizblock, in dem sie Befehle im Browser eingeben (auch dann, wenn Jupyter lokal installiert wurde). Wie bei der Python-Shell wird jeder Befehl sofort ausgeführt. Der große Vorteil ist, dass z.B. Listen, vor allem Datenstrukturen wie Matrizen, so ausgegeben werden, dass ihr Inhalt gut dargestellt wird. Auch Diagramme werden in die Ausgabe integriert. Weder muss Matplotlib installiert noch importiert werden. Das macht das Auswerten von Daten sehr komfortabel.

Da es so wichtig: Die Ausführung von Notebooks im Rahmen der Webplattform ist nur eine Option. Wurde Jupyter lokal installiert, steht grundsätzlich derselbe Komfort zur Verfügung. Jupyter ist damit ideal für das Lernen von Python geeignet und eine attraktive Alternative zur Python-Shell oder Idle.

Auch für ein lokal installiertes Jupyter lassen sich weitere Kernels hinzufügen, so dass auch in der Offline-Version andere Sprache als Python zur Auswahl stehen.

IPython

Wurde im letzten Abschnitt bereits erwähnt. War früher etwas Eigenes, ging dann in Juypter über, steht aber in der damaligen Version nach wie vor als etwas Eigenes zum Download zur Verfügung. In diesem Fall ist IPython eine komfortablere Python-Shell, die sich sehr gut zum Kennenlernen von Python eignet. Der Vorteil gegenüber Jupyter: Es muss deutlich weniger installiert werden, die Installation ist einfacher und am Ende arbeitet „man“ in einer vertrauten Shell, die aber mehr Komfort bietet und sich insgesamt „besser anfühlt“ als die etwas schlichte Python-Shell.

PyPy

PyPy ist ein Compiler für Python-Programme, der die Ausführung von Python-Programmen beschleunigen soll (ich habe damit aber noch nie gearbeitet). Inzwischen soll auch die Förderung des Projekts ausgelaufen sein, so dass die Fortführung des Projekts unsicher ist (dies ist aber lediglich meine persönliche Einschätzung und kein offizielles „Statement“).

https://de.wikipedia.org/wiki/PyPy

Umgang mit zweidimensionalen Feldern lernen – am Beispiel der Vigenére-Verschlüsselung

In diesem Beitrag möchte ich den Umgang mit zweidimensionalen Feldern in Python an einem sehr netten Beispiel veranschaulichen. Gleich vorweg, es gibt eigentlich keine mehrdimensionalen Felder in Python, sondern nur Listen, die als Elemente Listen enthalten. Und was bedeutet überhaupt zweidimensioal?

Ganz einfach, stellt Euch bitte kurz ein Brett vor, das aus horizontalen und vertikalen Reihen mit einer bestimmten und immer gleichen Anzahl an Feldern pro Reihe besteht. Bei einem Schachbrett gibt es bekanntlich acht Zeilen mit jeweils 8 Feldern. Also ein 8×8-Feld mit 64 Feldern. Bei einem Schiffe versenken-Spiel sind es 10 Reihen mit jeweils 10 Feldern. Also ein 10×10-Feld. Möchte man ein solches Brett mit seinen Feldern in einer Programmiersprache abbilden, benötigt man ein zweidimensionales Feld. Folglich gibt es auch zwei Indices. Für jede „Dimension“ ein Index.

Die allgemeine Schreibweise Feld[0,0] spricht das Feld in der linken, oberen Ecke an, die Schreibweise Feld[3,3] das Feld in der Mitte usw. Warum 0 und warum 3 und nicht 4? Ganz einfach, weil bei fast allen Programmiersprachen die Nummerierung bei 0 beginnt und nicht bei 1.

Wer sich im Internet ein wenig zum Thema „Python und Arrays“ umsieht findet schnell Beispiele, in denen Arrays verwendet werden. Kann Python daher doch Array?Nein und Ja. Nein, weil es Arrays offiziell nicht gibt. Ja, weil über Erweiterungen wie NumPy Arrays und Matrizen als eigene Datentypen hinzukommen. Im Folgenden geht es aber nur Listen, die wie Felder behandelt werden können.

Da das Beispielprogramm etwas umfangreicher ist und nicht jeder, der die Seite liest, sich bereits mit Feldern bei Python auskennt, zuerst ein Beispiel zur „Einstimmung“. Ich verwende aber mit Absicht keine Erweiterung wie NumPy, da es nur um die reine Python-Programmierung gehen soll.

Der erste Befehl legt eine leere Liste an:

f = []

Die Liste ist aber noch leer. Soll sie z.B. bereits zehn Nullen enthalten, ist List Comprehension praktisch, denn man spart sich eine for-Schleife mit Zuweisung:

f = [0 for i in range(0,10)]

Anschließend erhalten die ersten zwei oder drei Reihen ebenfalls eine Liste als Wert:

f[0] = [i for i in range(1,11)]
f[1] = [i for i in range(1,11)]
f[2] = [i for i in range(1,11)]

Natürlich hätte ich das Feld auch mit einem einzigen Befehl anlegen und befüllen können, aber so ist es etwas anschaulicher (dieser Befehl kommt in dem Beispielprogramm vor).

Jetzt wäre die Frage, wie ein Element, also eine Zahl, in der Listen-Liste angesprochen wird. Eigentlich sehr naheliegend durch folgende Schreibweise:

f[0][1]

Die bei anderen Programmiersprachen übliche Schreibweise f[0,1] geht bei Python nicht, da dies die Slicing-Schreibweise ist.

Jetzt zu dem eigentlichen Beispiel. Es veranschaulicht die Vigenére-Chiffre, die ich vor kurzem im Rahmen meines OMI-Studiums im Modul „Grundlagen der Informationssicherheit“ (GIS) kennengelernt habe.

Vigenére-Chiffre ermöglichen eine einfache“ Verschlüsselung“ Worten mit Hilfe eines Schlüssels, der wiederum nichts anderes als ein beliebiges Wort ist. Die Grundlage ist eine Tabelle mit z.B. 26 Spalten und 26 Zeilen. Warum 26? Damit alle Buchstaben des Alphabets in einer Zeile Platz haben. Die Tabelle ist so aufgebaut, dass jede Zeile die Buchstaben von A bis Z enthält, ab der zweiten Zeile aber immer um einen Buchstaben versetzt. Die erste Zeile beginnt mit A,B,C,D, die zweite mit B,C,D,E, die dritte mit C,D,E,F usw. Nach dem Z geht es in einer Zeile mit A wieder weiter.

Die Verschlüsselung besteht darin, dass das zu verschlüsselnde Wort Buchstabe für Buchstabe durchgegangen wird. Soll z.B. das Wort SOMMER verschlüsselt werden, gehen wir die Buchstaben S, O, M, M, E und R durch. Für jeden Buchstaben wird der Buchstabe mit jedem in Bezug auf die Position des Buchstabens korrespondierenden Buchstaben aus dem Schlüssel kombiniert. Heißt der Schlüssel z.B. GEHEIM, passt das S zum G, das O zum E, das M zum H usw. Der zweite Buchstabe legt die Zeile fest. Aus der Kombination Spalte und Zeile ergibt sich jeweils ein Buchstabe, der zum Chiffre hinzugefügt wird. Der Chiffre ist die verschlüsselte Form des Wortes.

Und was ist, wenn der Schlüssel kürzer ist als das zu verschlüsselnde Wort? Das ist natürlich kein Problem, wenn dann wird wieder der erste Buchstabe aus dem Schlüssel verwendet usw.

Wer das Ganze trotz meiner Bemühungen immer noch nicht verstanden hat, soll es bitte noch einmal bei Wikipedia nachlesen:

https://de.wikipedia.org/wiki/Vigen%C3%A8re-Chiffre

Dort wird alles natürlich viel komplizierter erklärt;)

Neben einer ausführlichen (und natürlich sehr guten) Erklärung findet ihr dort auch ein Beispiel für die von mir wortreich beschriebene Chiffre-Tabelle.

Doch nun zur Umsetzung in einem Python-Programm. Ich kann jedem nur empfehlen, die Aufgabe erst einmal selber zu lösen, da sie doch einiges an „Grundverständnis“ der Python-Programmierung abverlangt. Nicht, weil die Programmierung besonders kompliziert oder anspruchsvoll wäre, sondern weil es auf ein paar „Randbedingungen“ ankommt, die beim Programmieren immer das sprichwörtliche Salz in der Suppe sind.

Wenn man ein Wort erfolgreich verschlüsselt hat, möchte man es natürlich auch wieder lesbar machen. Das soll zunächst eine Übung sein, die jeder bitte alleine lösen soll, da die Lösung a) nicht schwer ist und ich b) niemanden das Erfolgserlebnis vorenthalten möchte. Probiert es einmal aus, es lohnt sich!

Das Beispiel findet ihr natürlich in meinem GitHub-Repo unter https://github.com/pemo11/pyhub. Am besten alle Beispiele (allzu viele sind es im Moment aber noch nicht) per git clone oder gleich in Visual Studio Code herunterladen (zur Not auch als Zip-Datei, wenn es mit den Git Tools für Windows wider Erwarten nicht funktionieren sollte).

# Vigenére Chiffre
# Umsetzung am 9.06.2020
# Autor: Peter Monadjemi
 
# Schritt 1: 26x26 Feld mit den jeweils pro Spalte um einen Buchstaben versetzen Buchstaben von A bis Z

# Thereotisch ließe sich das Fels auch gleich per List Comprehension passend füllen
vigFeld = [[0 for i in range(0,26)] for j in range(0,26)]

offset = 0

# Ergebnis stimmt, kann aber eventuell vereinfacht werden
for i in range(0,26):
    for j in range(0,26):
        c = j + offset + 65
        c = c - 26 if c > 90 else c
        vigFeld[i][j] = chr(c)
    offset += 1
 
for i in range(0,26):
    print(vigFeld[i])

# Das Wort "SEMESTER" soll mit dem Schlüssel SICHER verschüsselt werden
wort = "SEMESTER"
schluessel = "SICHER"
geheim = ""

k = 0
for c in wort:
    i = ord(c) - 65
    j = ord(schluessel[k]) - 65
    k = k + 1 if k < len(schluessel) - 1 else 0
    geheim += vigFeld[i][j]

# Stimmt - aus SEMESTER wird KMOLWKWZ
print(geheim)

# Wie muss eine Entschlüsselungsfunktion aussehen?

Ein Fussball-Ergebnis-Simulator

Die Corona-Pandemie verlangt von jedem von uns sehr viel ab. Dazu gehört auch ein Verzicht auf vertraute Gewohnheiten, auf Hobbies – vom Ausüben der beruflichen Tätigkeit gar nicht zu reden. Software-Entwickler haben es in solchen Zeit leichter, da sie in der Regel problemlos auf Homeoffice umstellen können. Zu den lieb gewonnenen Gewohnheiten, auf die wir noch eine Weile verzichten müssen, gehört auch das Verfolgen der Fußballbundesliga – am Fernseher, am Radio oder z.B. über den Kicker-Live-Ticker.

Das folgende Python-Programm simuliert ein Fußballspiel in der Form, dass jede Minute ein Ereignis auftritt, das vom einem „Live-Ticker“ in der Konsole gemeldet wird. Die Idee war, dass ein Fußballspiel ein Spiel mit einer insgesamt überschaubaren Anzahl an Grundereignissen ist (ein Spieler spielt einen Pass, ein Spieler begeht ein Foulspiel, ein Spieler erzielt ein Tor, der Torwart wehrt einen Ball ab usw.)

Ein Fussballspiel-Simulator muss also lediglich diese Ereignisse per Zufallsgenerator erzeugen und dafür sorgen, dass die Aufeinanderfolge dieser Ereignisse stimmig ist. Beispiel: Wenn Spieler der Mannschaft A einen Pass gespielt hat, der ins Seitenaus ging, muss die nächste Aktion ein Einwurf eines Spielers der Mannschaft B sein. Erzielt ein Spieler der Mannschaft A ein Tor, muss das nächste Ereignis der Anstoß durch einen Spieler der Mannschaft B sein.

Pro Minute tritt ein Ereignis auf, nach 90 Minuten ist Schluss und das Ergebnis wird angezeigt.

Eine kleine Besonderheit ist der Umstand, dass jede Mannschaft, die aus 6 Spielern bestehen muss, individuell zusammengestellt werden kann, da neben dem Namen des Spielers auch seine Passgenauigkeit, seine Dribblingstärke und seine Fairness in Prozent vorgegeben werden kann. Insofern können stärkere und schwächere Teams gegeneinander spielen.

Das vorgestellte Beispiel ist, wie alle anderen Beispiele, die ich in diesem Blog vorstelle, in erster Linie dazu gedacht, den Umgang mit der Programmiersprache Python zu veranschaulichen. Außerdem besitzen „Simulation“ immer einen gewissen Reiz. Auch wenn die Spielentscheidungen auf simplen Regeln basieren, wäre es natürlich kein Problem, mehr Intelligenz ins Spiel zu bringen, in dem die z.B. ein Spieler, dessen Passquote unter einen bestimmten Wert sind, ausgewechselt wird.

Das vollständige Beispiel gibt es in meinem Github-Portal unter https://github/pemo11/pyhub.

Die Programmdatei FussballSimulatorV1.py kann unter Python 3.x direkt ausgeführt werden. Es müssen keine Module installiert werden. Die beiden Mannschaften werden direkt im Programm definiert. Es wäre natürlich naheliegend, dass sie über eine Textdatei geladen werden.

# Ein PyFuba-Spielsimulator - V1
# PyFuba - Wie Fußball nur mit etwas anderen Regeln - es gibt nur 6 Feldspieler, keinen expliziten Torwart, keine Ecken und jeder darfs aufs Tor schießen
# Dies ist die erste Version, bei die die Einfachheit im Vordergrund steht
# Die Spieler spielen Pässe, die ins Seitenaus gehen können, begehen Fouls und schießen aufs Tor
# Es gibt keine gelben Karten und die Entfernung zum Tor spielt für die Trefferwahrscheinlichkeit keine Rolle, da die Spielerpositionen keine Rolle spielen
# Jede Minute tritt ein Spielereignis ein und am Ende kommt ein Spielstand zustande, der ausgegeben wird
# Autor: Peter Monadjemi, www.pyhub.de
# Letzte Aktualisierung: 31/03/2020

import time
import random

# Definiert einen Spieler mit einem Namen
class Spieler:

    def __init__(self, Name, Nr, Torgefahr, Dribbling, Pass, Fairness):
        self.Name = Name
        self.TrikotNr = Nr
        # Wahrscheinlichkeit, dass ein Schuss aufs Tor ein Treffer ist
        self.Torgefahr = Torgefahr
        # Wahrscheinlichkeit, dass der Spieler beim Dribbling den Ball verliert
        self.Dribbling = Dribbling
        # Wahrscheinlichkeit, dass ein Pass nicht beim Mitspieler ankommt
        self.Passgenauigkeit = Pass
        self.Fairness = Fairness

# Definiert ein Team mit einem Namen und Spielern
class Team:

    def __init__(self, Name, Spieler):
        self.Name = Name
        self.Spieler = Spieler

# Definiert die "Umgebung" für ein Spiel        
class SpielkontextA:

    def Spielpass(self, Spieler):
        self.Spielstatus = "Pass"
        Ergebnis = random.randint(0,100) <= Spieler.Passgenauigkeit
        return Ergebnis

    def Torschuss(self, Spieler):
        self.Spielstatus = "Torschuss"
        Ergebnis = random.randint(0,100) <= Spieler.Torgefahr
        return Ergebnis

    def Foulspiel(self, Spieler):
        self.Spielstatus = "Foul"
        Ergebnis = random.randint(0,100) <= Spieler.Fairness
        return Ergebnis

    def LaufMitBall(self, Spieler):
        self.Spielstatus = "Normal"
        Ergebnis = random.randint(0,100) <= Spieler.Dribbling
        return Ergebnis

    def __init__(self, Name, TeamHeim, TeamGast):
        self.Name = Name
        self.TeamHeim = TeamHeim
        self.TeamGast = TeamGast
        self.TeamSpiel = TeamHeim
        self.Spielstatus = "Anpfiff"
        self.ToreHeim = 0
        self.ToreGast = 0
        self.AktuellerSpieler = TeamHeim.Spieler[0]
        self.Aktionsliste = [(self.Spielpass,80), (self.Torschuss,20), (self.Foulspiel,10)]
        self.StandardAktion = self.LaufMitBall

    def SpielAktion(self):
        z = random.randint(0, len(self.Aktionsliste)-1)
        w = random.randint(0, 100)
        if w <= self.Aktionsliste[z][1]:
            return self.Aktionsliste[z][0](self.AktuellerSpieler)
        else:
            return self.StandardAktion(self.AktuellerSpieler)

    def Spielstatus(self):
        return "Spielstatus"

HTeam = Team("Team A", [Spieler("Arnold",1,20,20,20,20), Spieler("Aichstätter",2,30,20,20,20), Spieler("Alaquiz",3,40,20,20,20), Spieler("Arnautovic",4,60,20,20,20),Spieler("Ailton",5,70,20,20,20), Spieler("Asimowich",6,70,20,20,20)])
GTeam = Team("Team B", [Spieler("Beister",1,20,20,20,20), Spieler("Bernandez",2,30,20,20,20), Spieler("Boateng",3,40,20,20,20), Spieler("Bittencour",4,50,20,20,20), Spieler("Bierhoff",9,80,20,20,20), Spieler("Beckenhauer",10,80,20,20,20)])

Spielkontext = SpielkontextA("Standard", HTeam, GTeam)

Spieldauer = 45
Spielzeit = 1

# Spielstart mit einem Heimspieler
z = random.randint(0, len(Spielkontext.TeamSpiel.Spieler) - 1)
spieler = Spielkontext.TeamSpiel.Spieler[z]
Spielkontext.AktuellerSpieler = spieler

while Spielzeit <= Spieldauer:
    # Aktuellen Spielstatus abfragen
    if Spielkontext.Spielstatus == "Anpfiff":
        print(f"Minute {Spielzeit}: Anpfiff  - Anstoß durch Spieler {Spielkontext.AktuellerSpieler.Name}")
    elif Spielkontext.Spielstatus == "Foul":
        print(f"Minute {Spielzeit}: Spieler {Spielkontext.AktuellerSpieler.Name} begeht ein Foul.")
        # Das andere Team kommt zum Zug
        Spielkontext.TeamSpiel = Spielkontext.TeamGast if Spielkontext.TeamSpiel.Name == Spielkontext.TeamHeim.Name else Spielkontext.TeamHeim
        # Spieler aus dem anderen Team festlegen
        z = random.randint(0, len(Spielkontext.TeamSpiel.Spieler) - 1)
        Spielkontext.AktuellerSpieler = Spielkontext.TeamSpiel.Spieler[z]
        print(f"Minute {Spielzeit}: Freistoß fuer {Spielkontext.TeamSpiel.Name}")
        Spielkontext.Spielstatus == "Normal"
    elif Spielkontext.Spielstatus == "Torschuss":
        print(f"Minute {Spielzeit}: Spieler {Spielkontext.AktuellerSpieler.Name} schießt auf das Tor.")
        if SpielErgebnis:
            print(f"Minute {Spielzeit}: Tor fuer {Spielkontext.TeamSpiel.Name} durch Spieler {Spielkontext.AktuellerSpieler.Name}")
            if Spielkontext.TeamSpiel.Name == Spielkontext.TeamHeim.Name:
                Spielkontext.ToreHeim += 1
            else:
                Spielkontext.ToreGast += 1
            print(f"Minute {Spielzeit}: Neuer Spielstand: {str(Spielkontext.ToreHeim)}:{str(Spielkontext.ToreGast)}")
        # Das andere Team kommt zum Zug
        Spielkontext.TeamSpiel =  Spielkontext.TeamGast if Spielkontext.TeamSpiel.Name == Spielkontext.TeamHeim.Name else Spielkontext.TeamHeim
        # Spieler aus dem anderen Team festlegen
        z = random.randint(0, len(Spielkontext.TeamSpiel.Spieler) - 1)
        Spielkontext.AktuellerSpieler = Spielkontext.TeamSpiel.Spieler[z]
        if SpielErgebnis:
            print(f"Minute {Spielzeit}: Anstoß für Team {Spielkontext.TeamSpiel.Name} durch Spieler {Spielkontext.AktuellerSpieler.Name}")
        else:
            print(f"Minute {Spielzeit}: Abstoß für Team {Spielkontext.TeamSpiel.Name}")
    elif Spielkontext.Spielstatus == "Pass":
        if not SpielErgebnis:
            print(f"Minute {Spielzeit}: Pass von Spieler {Spielkontext.AktuellerSpieler.Name} gehts in Seitenaus")
            # Das andere Team kommt zum Zug
            Spielkontext.TeamSpiel =  Spielkontext.TeamGast if Spielkontext.TeamSpiel.Name == Spielkontext.TeamHeim.Name else Spielkontext.TeamHeim
            # Spieler aus dem anderen Team festlegen
            z = random.randint(0, len(Spielkontext.TeamSpiel.Spieler) - 1)
            Spielkontext.AktuellerSpieler = Spielkontext.TeamSpiel.Spieler[z]
            print(f"Minute {Spielzeit}: Einwurf fuer {Spielkontext.TeamSpiel.Name}  durch Spieler {Spielkontext.AktuellerSpieler.Name}")
        else:
            # Pass muss einen anderen Spieler aus dem Team erreichen
            SpielerAlt = Spielkontext.AktuellerSpieler
            while True:
                z = random.randint(0, len(Spielkontext.TeamSpiel.Spieler) - 1)
                if Spielkontext.TeamSpiel.Spieler[z].Name != Spielkontext.AktuellerSpieler.Name:
                    Spielkontext.AktuellerSpieler = Spielkontext.TeamSpiel.Spieler[z]
                    break
            print(f"Minute {Spielzeit}: Spieler {SpielerAlt.Name} spielt einen Pass zu {Spielkontext.AktuellerSpieler.Name}")
    else:
        print(f"Minute {Spielzeit}: Spieler {Spielkontext.AktuellerSpieler.Name} läuft mit dem Ball")
        if not SpielErgebnis:
            # Das andere Team kommt zum Zug
            Spielkontext.TeamSpiel =  Spielkontext.TeamGast if Spielkontext.TeamSpiel.Name == Spielkontext.TeamHeim.Name else Spielkontext.TeamHeim
            AlterSpieler = Spielkontext.AktuellerSpieler
            # Spieler aus dem anderen Team festlegen
            z = random.randint(0, len(Spielkontext.TeamSpiel.Spieler) - 1)
            Spielkontext.AktuellerSpieler = Spielkontext.TeamSpiel.Spieler[z]
            print(f"Minute {Spielzeit}: Spieler {AlterSpieler.Name} verliert den Ball an Spieler {Spielkontext.AktuellerSpieler.Name}")

    # Nächste Aktion festlegen
    SpielErgebnis = Spielkontext.SpielAktion()

    time.sleep(1)
    Spielzeit += 1

print(f"Minute {Spielzeit}: Schlusspfiff")
print(f"Minute {Spielzeit}: Endstand {Spielkontext.TeamHeim.Name} gegen {Spielkontext.TeamGast.Name} {str(Spielkontext.ToreHeim)}:{str(Spielkontext.ToreGast)}")

Ein Latein-Quiz als hervoragende Gelegenheit, die Leichtigkeit der Programmierung mit Python zu demonstrieren

Auch wer mit Latein nicht allzu viel am Hut hat, sollte sich das folgende Beispiel anschauen bzw. umsetzen, da man natürlich nicht nur Zitate auf Latein abfragen kann, sondern beliebige andere Themen. In erster Linie ist es aber (Vorsicht Eigenlob) ein gelunges Beispiel, dass die Leichtigkeit der Python-Programmierung und den flexiblen Umgang mit Arrays veranschaulicht.

Es kommen keine Hashtables vor, keine Klassen, keine Regexes, keine exotischen Module, sondern nur die „Basics“.

Ausgangspunkt ist eine Textdatei mit einer Reihe von Zeilen. Jede Zeile enthält ein Zitat auf Latein, einen Doppelpunkt und die deutsche Übersetzung:

Abyssus abyssum invocat:Ein Fehler zieht den anderen nach sich.
Ad absurdum:Etwas als unsinnig nachweisen.
Alea iacta est:Der Würfel ist gefallen (Zitat von Caesar)

Die Anzahl der Zeilen spielt keine Rolle. WIchtig ist lediglich, dass pro Zeile nur ein Doppelpunkt vorkommen darf.

Auf dieser Textdatei soll ein Quiz gemacht werden. Das bedeutet, dass z.B. 10 Fragen ausgewählt werden. Jede Frage besteht aus einem lateinischen Zitat und vier (willkürlich) Antworten, von denen jeweils eine richtig ist. Also der klassische Multiplechoice-Test.

Die Programmierherausforderung besteht darin, dass z.B. zehn Zitate ausgewählt und pro Zitat z.B. vier Antworten und die Information, welche der vier Antworten richtig ist. Eine kleine Schwierigkeitssteigerung besteht darin, dass keine der vier Antworten doppelt vorkommen und die richtige Antwort natürlich dabei sein muss. Diese darf aber nicht immer die erste Antwort sein, sondern xmuss ebenfalls zufällig über die vier Antworten verteilt werden.

Wie das folgende Listing zeigt, ist die Umsetzung gar nicht sooo schwer, wenn man bereits etwas „pythonisch“ denkt und von der Flexiblität beim Umgang mit Arrays bzw. Listen und den sehr praktischen List Comprehensions Gebrauch macht.

Da ich wie sooft nicht die Zeit habe, die Schritte einzeln vorzustellen, paste ich zunächst das gesamte Listing in den Blog-Eintrag. Das meiste sollte selbsterklärend sein. Falls es Fragen geben sollte, stehe ich natürlich zur Verfügung und helfe mit Erklärungen aus.

Wie immer gibt es Raum für Verbesserungen. So müsste es eine Option geben, um die „Richtung“ der Übersetzung festlegen zu können. Die Anzahl der Fragen müsste variabel und wie bei jedem guten Quiz müsste es eine Option geben, die bewirkt, dass alle falsch beantworten Fragen solange wiederholt werden bis es keine falschen Antworten mehr gab.

Noch netter wäre es natürlich, wenn das Programm keine Konsolenanwendung, sondern eine App wäre. Dann wäre es aber sehr viel einfacher, sie anstatt mit Python mit ein wenig JavaScript als reine Browser-App umzusetzen. Falls doch Python ins Spiel kommen soll, könnten die Fragen über eine Flask-Webapp zur Verfügung gestellt werden. Aber dann wäre es wieder richtige Programmierung und genau darum soll es mit dem kleinen Beispiel nicht gehen.

# Ein Latein-Merkquiz
import os
import random

basePfad = os.path.dirname(__file__)
txtPfad = os.path.join(basePfad, "LateinZitate.txt")

zitate = []
with open(txtPfad, encoding="UTF-8") as fh:
    for zitat in fh:
        if zitat[-1] == "\n":
            zitat = zitat[:-1]
        zitate.append(zitat.split(":"))

anzahl = 10
fragen = []

for i in range(1, anzahl+1):
    while True:
        z = random.randint(0, len(zitate)-1)
        frage = zitate[z][0]
        if len([f for f in fragen if f[0] == frage]) == 0:
            antworten = ["","","",""]
            aIndex = random.randint(0, 3)
            antworten[aIndex] = zitate[z][1]
            # Drei weitere Antworten suchen, die nicht gleich der Frage sind
            for j in [k for k in range(0,4) if k != aIndex]:
                while True:
                    z1 = random.randint(0, len(zitate)-1)
                    antwort = zitate[z1][1]
                    if not antwort in antworten:
                        break
                antworten[j] = antwort
            fragen.append((frage, antworten, aIndex))
            break
print(f"Das Quiz ist mit {anzahl} Fragen bereit.")

print(fragen)

anzahlRichtig = 0
anzahlFalsch = 0

for i in range(1, anzahl):
    print("Was bedeutet %s?" % fragen[i][0])
    for j, antwort in enumerate(fragen[i][1]):
        print(chr(j+65) + ". " + antwort)
    a = input("A,B,C oder D oder Q für Abbruch?")
    if a == "Q":
        break
    antwort = fragen[i][2]
    if ord(a)-65 == antwort:
        print("Die Antwort war richtig!")
        anzahlRichtig += 1
    else:
        print("Leider falsch!")
        print("Richtig wäre %s" % fragen[i][1][antwort])
        anzahlFalsch += 1

print(f"Richtige Antworten: {anzahlRichtig} Falsche Antworten: {anzahlFalsch} - Deine Quote: {(anzahlRichtig/anzahl):.0%}")


Tipp des Tages – Vorhandene Module und Packages auflisten

Bei Python dreht sich fast alles um Module, die entweder Teil der Python-Installation sind oder nachträglich über Packages hinzugekommen sind. Je nach dem, welche Python-Installationen auf einem Rechner vorhanden sind, kann ein Package auf mehrfach installiert und in unterschiedlichen Verzeichnissen abgelegt worden sein.

Eine Liste aller Module liefert der Befehl help("modules").

Eine Liste aller installierten Packages liefert ein pip list, das aber außerhalb der Python-Shell eingegeben wird.

Tipp des Tages: Anzahl der Leerzeichen zu Beginn einer Zeichenkette zählen

Ob eine Zeichenkette mit einem oder mehreren Leerzeichen beginnt, lässt sich über die startswith()-Function des str-Objekts, dem die Zeichenkette zugrundeliegt, sehr einfach feststellen.

Möchte man aber die genaue Zahl der Leerzeichen, wird es etwas aufwändiger. Wer wie ein klassischer Entwickler denkt, versucht es vielleicht mit einer Schleife, die alle Zeichen bis zum nächsten Zeichen durchgeht, das kein Leerzeichen ist. Mit Hilfe eines regulären Ausdrucks (auch Regex genannt), geht es ein wenig einfacher.

Voraussetzung ist, dass das Modul re importiert wurde. Dann gibt es u.a. die findall()-Function, die mit zwei Werten aufgerufen wird:

>Einem regulären Ausdruck

>Dem Text, in dem die „Muster“ gefunden werden sollen

Der Text ist die Zeichenkette, die mit Leerzeichen beginnt. Das war einfach. Beim Muster wird es etwas „komplizierter“, denn man muss sich natürlich ein wenig mit der Schreibweise regulärer Ausdrücke auskennen.

Die Erklärung in absoluter Kurzform:

\s steht für ein Leerzeichen

der folgende * steht für 0, 1 oder beliebig viele Zeichen, in diesem Fall also Leerzeichen

durch die runden Klammer wird eine Gruppe gebildet

der Punkt steht für ein beliebiges Zeichen

der folgende * steht wieder für 0, 1 oder beliebig viele Zeichen, in diesem Fall kann es ein beliebiges Zeichen sein

Der Aufruf von findall() gibt eine Liste aller „Treffer“zurück. Und da der Treffer aus zwei Gruppen besteht, resultiert eine Liste mit Listen. Da es um die erste Gruppe im ersten Treffer geht, sieht der Aufruf wie folgt aus:

t = "    abc"
len(re.findall("(\s*)(.*)",t)[0][0])

Reguläre Ausdrücke wirken auf den ersten Blick unglaublich kryptisch und kompliziert. Sie sind es aber nicht bzw. mit einfachen Grundregeln kommt ihr bereits sehr weit. Die wichtigste Merkregel für den Anfang ist immer: Der * ist kein Platzhalter (sondern eine Mengenangabe) und der unscheinbare Punkt steht für ein beliebiges Zeichen.

Tipp des Tages: Eine Zeichenkette umdrehen

Der folgende Tipp fällt eher in die Kategorie „Spielerei“ bzw. „Was mit Slicing alles möglich ist“ (die Idee stammt auch nicht von mir, sondern von einem Teilnehmer meines letzten Python-Kurses).

Folgende Aufgabenstellung: Ausgabe einer Zeichenkette in umgedrehter Reihenfolge. Aus „Python ist ganz nett“ soll z.B. „tten znag tsi nohtyP“ werden.

Wer bei Python denkt wie ein Entwickler, versucht es vielleicht mit einer for-Wiederholung, die am Ende der Zeichenkette beginnt und Zeichen für eine Zeichen eine neue Zeichenkette zusammensetzt:

s1 = "Python ist ganz nett"
s2 = ""
for i in range(len(s1)-1,-1 ,-1):
    s2 += s1[i]

Das ist ok, aber für Python doch etwas umständlich. Sehr viel einfacher geht es dank der universellen Slicing-Schreibweise, die sich auf jede Liste anwenden lässt. Und da bei Python ein String ebenfalls eine Liste (von Zeichen) ist, funktioniert Slicing auch bei Strings (und stellt den Ersatz für eventuell vertraute Functions wie Left, Right oder Substr dar, die es bei Python nicht gibt).

Python ist ganz nett"[::-1]

Ich muss zugeben, dass ich darauf von alleine nicht unbedingt gekommen wäre, aber wenn man es so sieht, ist es ja eigentlich ganz logisch;)