Skip to content

Blockkurs Python

English version

There is a (otherwise identical) English version.

Zielgruppe: Programmieranfänger (auch ohne Vorkenntnisse) aus dem Zeitraum Bachelor bis Promotion. Entwickelt an der Universität Kassel.

Wir lösen ein konkretes Problem im Laufe von fünf Tagen: wir schreiben eine Partikelsimulation, die in 2D die Bewegung von Teilchen berechnet. Mit dieser Methode lassen sich sowohl Sterne im Universum als auch Atome in einem Molekül verfolgen: die mathematischen Grundlagen und algorithmischen Details sind sich bei diesem Problem erstaunlich ähnlich.

Programmieren ist wie Fahrrad fahren: man lernt es nicht aus einem Buch. Daher gibt es keine Folien, sondern nur Code. Aller Code wird in Moodle verfügbar gemacht - so wie wir ihn besprochen haben, nicht aber vorab. Ziel ist hier, alle zu motivieren, nicht die Lösung abzunicken, sondern selbst auszuprobieren.

In jedem der Module beleuchten wir nur ein Minimum der Themen um die kognitive Last so gering wie möglich zu halten. Die Sprache enthält noch mehr Konstrukte und Funktionalität als hier dargestellt, aber das ist für den Anfang zu viel. Wir entwickeln also nicht die kompakteste Lösung zu den Aufgaben, aber man erhält einen kompletten Werkzeugkasten mit dem die ersten Projekte gestemmt werden können.

Grundlagen: Sprachkonstrukte und Datentypen

Fingerübungen
  1. Schreiben Sie eine Funktion add(a: int, b: int) -> int, die zwei Zahlen addiert.

    Lösung
    def add(a: int, b: int) -> int:
        return a + b
    
  2. Schreiben Sie eine Funktion is_even(n: int) -> bool, die prüft, ob eine Zahl gerade ist.

    Lösung
    def is_even(n: int) -> bool:
        return n % 2 == 0
    
  3. Schreiben Sie eine Funktion absolute(n: float) -> float, die den Betrag einer Zahl berechnet.

    Lösung
    def absolute(n: float) -> float:
        if n >= 0:
            return n
        else:
            return -n
    
  4. Erstellen Sie eine Liste mit den ersten fünf Buchstaben des Alphabets. Erzeugen Sie dann eine Kopie derselben und fügen Sie in der Kopie die nächsten fünf Buchstaben an. Prüfen Sie, dass die erste Liste weiterhin nur die ersten fünf Buchstaben enthält.

    Lösung
    alphabet = ["a", "b", "c", "d", "e"]
    alphabet_copy = alphabet[:]
    alphabet_copy += ["f", "g", "h", "i", "j"]
    print(alphabet)
    print(alphabet_copy)
    
  5. Erzeugen Sie ein Tupel, das eine Zahl, ihr Quadrat und ihr Negatives enthält.

    Lösung
    t = (3, 3**2, -3)
    print(t)
    
  6. Erzeugen Sie ein Dictionary, das fünf chemischen Elementen ihre Ordnungszahl zuweist.

    Lösung
    elements = {"H": 1, "He": 2, "Li": 3, "Be": 4, "B": 5}
    print(elements)
    
  7. Erzeugen Sie eine Liste der eindeutigen Einträge eines Tuples. Nutzen Sie dafür ein Set.

    Lösung
    t = (1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
    s = set(t)
    l = list(s)
    print(s)
    
  8. Nutzen Sie eine for-Schleife, um die Zahlen 1-10 auszugeben.

    Lösung
    for i in range(1, 11):
        print(i)
    
  9. Schreiben Sie eine Funktion print_until(n: int), die die Zahlen 1 bis n ausgibt.

    Lösung
    def print_until(n: int):
        for i in range(1, n+1):
            print(i)
    
  10. Schreiben Sie eine Funktion sum_until(n: int) -> int, die die Summe der Zahlen 1 bis n berechnet.

    Lösung
    def sum_until(n: int) -> int:
        total_sum = 0
        for i in range(1, n + 1):
            total_sum += i
        return total_sum
    
Aufgaben für Anfänger
  1. Schreiben Sie eine Funktion center_of_mass(positions: list[int], masses: list[float]), die in 1D den Schwerpunkt einer Punktewolke errechnet.
  2. Berechnen Sie den nächsten Stern aus einer Liste von Sternen mit 2D-Koordinaten und gebe den Index des nächsten Sterns zurück: nearest_neighbor(me: tuple[float, float], others: list[tuple[float, float]]).
  3. Wieviele Zahlen unter 10000 sind Palindrome?
  4. Schreiben Sie eine Funktion, die Änderung der Position eines Teilchens in 2D basierend auf seiner aktuellen Geschwindigkeit und einer gegebenen Zeitspanne berechnet.
  5. Schreiben Sie eine Funktion, in der sich ein einziges Teilchen zwischen zwei Wänden hin- und herbewegt. Die Funktion gibt die Positionen des Teilchens im Zeitverlauf zurück und nimmt den Zeitschritt als Argument an.
  6. Schreiben Sie eine Funktion, die eine Liste von chemischen Elementen erhält und eine Summenformel ausgibt: sum_formula(["O", "C", "O"]) ergäbe CO2.
  7. Schreiben Sie eine Funktion, die zwei beliebig lange Vektoren addiert und das Vektorergebnis als Liste zurückgibt.
  8. Schreiben Sie eine Funktion, die eine Matrix als Liste von Listen erstellt, bei der die Diagonalen Werte von 1 bis n enthalten und alle anderen Werte 0 sind.
  9. Schreiben Sie eine Funktion, die das Produkt aller Zahlen in einer Liste berechnet.
  10. Schreiben Sie eine Funktion, die aus einer Liste von Argumenten ein Dictionary erstellt, das zählt, wie oft jedes Argument vorkommt.
  11. Schreiben Sie eine Funktion, set_intersection(a: list[int], b: list[int]), die die Schnittmenge zweier Listen zurückgibt.
  12. Schreiben Sie eine Funktion is_prime(n: int), die prüft, ob eine Zahl eine Primzahl ist.
  13. Schreiben Sie eine Funktion count_three_digit_numbers(list[int]), die zählt, wie viele Zahlen in einer Liste dreistellig sind.
  14. Schreiben Sie eine Funktion classify_number(n: int) -> str, die eine Zahl als "positiv", "negativ" oder "null" klassifiziert.
  15. Schreiben Sie eine Funktion count_electrons(elements: list[str]), die die Anzahl der Elektronen berechnet, die nötig sind, damit eine Summenformel elektrisch neutral ist.
Aufgaben für Fortgeschrittene
  1. Schreiben Sie eine Funktion in der die Gravitationskraft beliebig vieler Sterne in 2D berechnet wird.
  2. Implementieren Sie die numerische Integration der Bewegung der Sterne mittels des Verlet-Algorithmus.
  3. Schreiben Sie eine Funktion, n_th_largest_number(n: int, numbers: list[int]), die das n-größte Element in einer Liste von Zahlen zurückgibt, ohne die Liste vollständig zu sortieren.
  4. Schreiben Sie eine Funktion sort_list(list[int]), die eine Liste von Zahlen sortiert ohne eingebaute Sortierungs-Funktionen zu verwenden.
  5. Schreiben Sie eine Funktion evaluate_and_multipy(argument: float, *args) -> float, die eine beliebige Anzahl an Funktionen (in *args) an der Stelle argument auswertet und deren Ergebnisse multipliziert.

    def evaluate_and_multipy(argument: float, *args) -> float:
        ...
    
    def calculate_one_thing(x: float) -> float:
        ...
    
    def calculate_another_thing(x: float) -> float:
        ...
    
    # berechne calculate_one_thing(3) * calculate_another_thing(3)
    evaluate_and_multipy(3, calculate_one_thing, calculate_another_thing)
    
  6. Berechnen Sie das die 2**n-te Potenz einer Zahl ohne die Potenzfunktion sondern mit einer Schleife und wiederholtem Aufrufen einer Funktion squared(x: float) -> float.

    def squared(x: float) -> float:
        ...
    
    def get2npower(x: float, n: int) -> float:
        # call squared() from here
        ...
    
Aufgaben für Profis
  1. Ohne es auszuführen: was gibt True, True, True == (True, True, True) zurück?
  2. Schreiben Sie eine Funktion, die Rechenaufgaben wie 3*(2+3)/7+1 als Zeichenkette akzeptiert und mit korrekter Klammerauflösung berechnet. eval() ist hier nicht gestattet.
  3. Zunächst gerne auch ohne Programmieren: Wieviele sechsstellige Zahlen sind Palindrome und gleichzeitig durch 11 teilbar?
  4. Warum ist not True == False aber ~True == -2?

Numerische Effizienz: Numpy und JAX

Übungsaufgaben
  1. Erzeugen Sie eine 3x3-Matrix, auf deren Diagonalen 42, 4711 und 110 stehen.
  2. Erzeugen Sie eine 3x3-Matrix mit fortlaufenden Zahlen in den Zeilen, beginnend bei 42. Berechnen Sie die Zeilensummen (Ergebnis: 129, 138, 147).
  3. Schreiben Sie eine Funktion pad_me(a: np.ndarray, pads: int, values: float) -> np.ndarray, die ein 2D-Array a um pads Einträge in alle Richtungen erweitert und die neuen Werte mit values füllt.
  4. Schreiben Sie eine Funktion normalize_vector(a: np.ndarray) -> np.ndarray, die einen Vektor normiert.
  5. Schreiben Sie eine Funktion random_zeros(n: int, m: int, p: float) -> np.ndarray, die eine nxm-Matrix mit Nullen und Einsen erzeugt, wobei die Wahrscheinlichkeit für eine 1 in jedem Eintrag p ist.
  6. Schreiben Sie eine Funktion closest_value(values: np.ndarray, query: float) -> float, die den Wert aus values zurückgibt, der dem Wert query am nächsten ist.
  7. Schätzen Sie die Wahrscheinlichkeit, dass zwei gleichverteilte Zufallszahlen zwischen 0 und 1 einen Abstand von weniger als 0.1 haben.
  8. Schreiben Sie eine Funktion random_walk(n: int) -> np.ndarray, die eine eindimensionale Zufallsbewegung mit n Schritten simuliert. Dabei ist jeder Schritt gleichförmig zwischen -1 und 1.
  9. Schreiben Sie eine Funktion, die den Rang einer Matrix errechnet.
  10. Schreiben Sie eine Funktion get_n_th_largest(n: int, values: np.ndarray) -> np.ndarray, die das n-größte Element in einem Array von Zahlen zurückgibt, ohne das Array vollständig zu sortieren.
  11. Schreiben sie eine Funktion convert_base_n(digits: np.ndarray, base: int) -> int, die eine Liste von Ziffern in einer gegebenen Basis in eine Dezimalzahl umwandelt.
  12. Schreiben Sie eine Funktion, die eine Matrix der Größe 2nx2n in eine Matrix der Größe nxn umwandelt, indem sie die Werte in 2x2-Blöcken mittelt.
  13. Schreiben Sie eine Funktion sort_by_column(a: np.ndarray, column: int) -> np.ndarray, die eine Matrix nach einer Spalte sortiert.
  14. Schreiben Sie eine Funktion make_row_sums_zero(a: np.ndarray) -> np.ndarray, die die Zeilensummen einer Matrix auf 0 setzt, indem die Diagonale der Matrix geändert wird.
  15. Schreiben Sie eine Funktion swap_row_and_column(a: np.ndarray, i: int, j: int) -> np.ndarray, die die i-te Zeile und die j-te Spalte einer Matrix vertauscht.
Aufgaben für Profis
  1. Benutzen Sie np.einsum, um das Matrixprodukt @, das Skalarprodukt np.dot und das äußere Produkt np.outer und die Spur np.trace zu berechnen.
  2. Schreiben Sie eine numpy-Funktion, die paarweise Abstände zwischen Vektoren berechnet, ohne Schleifen zu verwenden und ohne die symmetrischen Abstände doppelt zu berechnen.
  3. Schreiben Sie eine jit-Funktion, die den Abstand zweier Teilchen berechnet, sofern der Abstand kleiner als ein Schwellwert ist. Die Funktion soll aus Effizienzgründen nicht immer den exakten Abstand berechnen, sondern nur, wenn die Teilchen nahe genug sind.
  4. Finden Sie einen Weg, um mittels np.random.uniform das Volumen einer 3D-Kugel mit Radius r zu berechnen. Finden Sie dann einen Weg, dieses Problem nur mit 1D-Arrays zu lösen.
  5. Schreiben Sie eine Funktion poly_product(*args: np.ndarray) -> callable, die eine Funktion zurückgibt, die das Produkt von Polynomen evaluiert. Die Polynome sind als 1D-Arrays gegeben, wobei der Koeffizient an der Stelle i den Koeffizienten von x**i darstellt.
Anwendungsaufgabe für Alle

Wir wollen eine Partikelsimulation schreiben, die die Bewegung von Teilchen in 2D berechnet. Das erfolgt in Schritten:

  1. Schreiben Sie eine Funktion init_universe(n: int) -> tuple[np.ndarray, np.ndarray], die zufällige Positionen und Geschwindigkeiten für n Teilchen generiert.
  2. Schreiben Sie eine Funktion update_positions(positions: np.ndarray, velocities: np.ndarray, dt: float, bounds: tuple[float, float]) -> np.ndarray, die die Positionen der Teilchen basierend auf ihren Geschwindigkeiten und einer gegebenen Zeitspanne dt berechnet. Dabei dürfen die Grenzen des Universums nicht verlassen werden. bounds gibt zusammen mit dem Ursprung die Grenzen des Universums.
  3. Schreiben Sie eine Funktion build_trajectory(n: int, t: int, dt: float, bounds: tuple[float, float]) -> np.ndarray, die die Positionen der Teilchen im Zeitverlauf berechnet. Mittels folgendem Code können Sie die Bewegung der Teilchen visualisieren und kontrollieren (eine Erklärung des Codes folgt zum späterem Zeitpunkt):

    import matplotlib.pyplot as plt
    
    # Grenzen des Universums
    bounds = (10, 8)
    
    # Beispielpositionen. Hier wird das Ergebnis von build_trajectory eingefügt.
    positions = np.zeros((10,2))
    positions[:, 0] = abs(np.arange(10)-5)
    positions[:, 1] = abs(np.arange(10)-7)
    
    plt.plot(positions)
    plt.plot((0,0,bounds[0], bounds[0], 0), (0, bounds[1], bounds[1], 0, 0), color="red")
    plt.xlabel("Erste Koordinate")
    plt.ylabel("Zweite Koordinate")
    
    Beispiel

    Hier ist die rote Box die Grenze des Universums. Wenn Sie alles richtig gemacht haben, bleiben die Teilchen innerhalb der Box.

  4. Bislang interagieren die Teilchen nicht. Geben Sie jedem Teilchen eine Masse und berechnen Sie die Gravitationskraft zwischen den Teilchen und summieren Sie diese auf in der Funktion get_force_vector(positions: np.ndarray, masses: np.ndarray) -> np.ndarray. Setzen Sie dabei zunächst die Gravitionskonstante und alle Einheiten auf 1.

  5. Unter Verwendung des Velocity-Verlet-Algorithmus können Sie nun die Geschwindigkeiten der Teilchen in der Funktion update_velocities(positions: np.ndarray, velocities: np.ndarray, masses: np.ndarray, dt: float, bounds: tuple[float, float]) -> np.ndarray berechnen.
  6. Aktualisieren Sie die Funktion build_trajectory so, dass die Geschwindigkeiten der Teilchen berücksichtigt werden.
  7. Finden Sie passende Parameter, die die Teilchen einander umkreisen lassen.
  8. Ersetzen Sie die Gravitationskraft durch ein Lennard-Jones-Potential, dessen Ableitung (also die Kräfte) Sie mit JAX berechnen. Damit man zwischen Gravitationskraft und LJ-Potential wechseln kann, ergänzen Sie build_trajectory um ein Argument force_callable, mit dem eine Funktion übergeben wird, die die Kräfte berechnet. Deren Signatur ist force_callable(positions: np.ndarray, masses: np.ndarray) -> np.ndarray.
  9. Jetzt wollen wir der Simulation genauer auf die Finger schauen. Ergänzen Sie die Funktion build_trajectory um ein Argument observers, das ein Dictionary übergeben bekommt, in dessen Schlüssel ein Name und in derem Wert eine Funktion enthalten ist, deren Ergebnis nach jedem Schritt aufgezeichnet wird. Die Funktion build_trajectory gibt nun ein Dictionary zurück das in positions die Koordinaten der Trajektorie enthält und ansonsten in jedem Schlüssel die Zeitreihe des jeweiligen Eintrags in observers. Jede Funktion in observers hat die Signatur observer(positions: np.ndarray, masses: np.ndarray, velocities: np.ndarray) -> np.ndarray. Schreiben Sie Funktionen für observers, die die kinetische Energie und die potentiale Energie des Systems berechnen. Sie können die Ergebnisse visualisieren:

    def kinetic_energy(positions, masses, velocities):
        return ...
    
    
    def potential_energy(positions, masses, velocities):
        return ...
    
    
    results = build_trajectory(
        ...,
        observers={"kinetic_energy": kinetic_energy, "potential_energy": potential_energy},
    )
    plt.plot(results["kinetic_energy"])
    plt.xlabel("Zeitschritt")
    plt.ylabel("Kinetische Energie")
    plt.show()
    plt.plot(results["potential_energy"])
    plt.xlabel("Zeitschritt")
    plt.ylabel("Potentielle Energie")
    
  10. Variieren Sie die Zeitschritte und Parameter. Was fällt Ihnen auf?

Optionale Themen, wenn wir gut vorankommen

Architektur: Objekt-orientiertes Programmieren

Trennung der Logik in Komponenten

  • Klassen vs Instanzen vs Funktionen
  • Konstruktoren
  • Vererbung
  • Komposition vs Vererbung
  • Private und geschützte Attribute und Methoden
  • Datenklassen

Algorithmische Effizienz

Ausblick auf Optimierungsmethoden und numerische Methoden

Analyse und Visualisierung: Pandas und Matplotlib

Verständnis des Data Science-Ökosystems