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
-
Wert-Datentypen
Zahlen, Zeichen und Wahrheit.
-
Variablen
Platzhalter und Änderungen.
-
Container-Datentypen
Alles hat seinen Platz.
-
Bedingungen
Wenn Dinge einmal anders sind.
-
Schleifen
Wiederholungen und Wieder-holungen.
-
Funktionen
Aufgaben delegieren.
Fingerübungen
-
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
-
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
-
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
-
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)
-
Erzeugen Sie ein Tupel, das eine Zahl, ihr Quadrat und ihr Negatives enthält.
Lösung
t = (3, 3**2, -3) print(t)
-
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)
-
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)
-
Nutzen Sie eine for-Schleife, um die Zahlen 1-10 auszugeben.
Lösung
for i in range(1, 11): print(i)
-
Schreiben Sie eine Funktion
print_until(n: int)
, die die Zahlen 1 bisn
ausgibt.Lösung
def print_until(n: int): for i in range(1, n+1): print(i)
-
Schreiben Sie eine Funktion
sum_until(n: int) -> int
, die die Summe der Zahlen 1 bisn
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
- Schreiben Sie eine Funktion
center_of_mass(positions: list[int], masses: list[float])
, die in 1D den Schwerpunkt einer Punktewolke errechnet. - 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]])
. - Wieviele Zahlen unter 10000 sind Palindrome?
- Schreiben Sie eine Funktion, die Änderung der Position eines Teilchens in 2D basierend auf seiner aktuellen Geschwindigkeit und einer gegebenen Zeitspanne berechnet.
- 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.
- Schreiben Sie eine Funktion, die eine Liste von chemischen Elementen erhält und eine Summenformel ausgibt:
sum_formula(["O", "C", "O"])
ergäbeCO2
. - Schreiben Sie eine Funktion, die zwei beliebig lange Vektoren addiert und das Vektorergebnis als Liste zurückgibt.
- 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.
- Schreiben Sie eine Funktion, die das Produkt aller Zahlen in einer Liste berechnet.
- Schreiben Sie eine Funktion, die aus einer Liste von Argumenten ein Dictionary erstellt, das zählt, wie oft jedes Argument vorkommt.
- Schreiben Sie eine Funktion,
set_intersection(a: list[int], b: list[int])
, die die Schnittmenge zweier Listen zurückgibt. - Schreiben Sie eine Funktion
is_prime(n: int)
, die prüft, ob eine Zahl eine Primzahl ist. - Schreiben Sie eine Funktion
count_three_digit_numbers(list[int])
, die zählt, wie viele Zahlen in einer Liste dreistellig sind. - Schreiben Sie eine Funktion
classify_number(n: int) -> str
, die eine Zahl als "positiv", "negativ" oder "null" klassifiziert. - 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
- Schreiben Sie eine Funktion in der die Gravitationskraft beliebig vieler Sterne in 2D berechnet wird.
- Implementieren Sie die numerische Integration der Bewegung der Sterne mittels des Verlet-Algorithmus.
- 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. - Schreiben Sie eine Funktion
sort_list(list[int])
, die eine Liste von Zahlen sortiert ohne eingebaute Sortierungs-Funktionen zu verwenden. -
Schreiben Sie eine Funktion
evaluate_and_multipy(argument: float, *args) -> float
, die eine beliebige Anzahl an Funktionen (in*args
) an der Stelleargument
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)
-
Berechnen Sie das die
2**n
-te Potenz einer Zahl ohne die Potenzfunktion sondern mit einer Schleife und wiederholtem Aufrufen einer Funktionsquared(x: float) -> float
.def squared(x: float) -> float: ... def get2npower(x: float, n: int) -> float: # call squared() from here ...
Aufgaben für Profis
- Ohne es auszuführen: was gibt
True, True, True == (True, True, True)
zurück? - 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. - Zunächst gerne auch ohne Programmieren: Wieviele sechsstellige Zahlen sind Palindrome und gleichzeitig durch 11 teilbar?
- Warum ist
not True == False
aber~True == -2
?
Numerische Effizienz: Numpy und JAX
-
NumPy
Numerische Effizienz.
-
JAX
Kompilierung und Differenzierung.
Übungsaufgaben
- Erzeugen Sie eine 3x3-Matrix, auf deren Diagonalen 42, 4711 und 110 stehen.
- Erzeugen Sie eine 3x3-Matrix mit fortlaufenden Zahlen in den Zeilen, beginnend bei 42. Berechnen Sie die Zeilensummen (Ergebnis: 129, 138, 147).
- Schreiben Sie eine Funktion
pad_me(a: np.ndarray, pads: int, values: float) -> np.ndarray
, die ein 2D-Arraya
umpads
Einträge in alle Richtungen erweitert und die neuen Werte mitvalues
füllt. - Schreiben Sie eine Funktion
normalize_vector(a: np.ndarray) -> np.ndarray
, die einen Vektor normiert. - 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 Eintragp
ist. - Schreiben Sie eine Funktion
closest_value(values: np.ndarray, query: float) -> float
, die den Wert ausvalues
zurückgibt, der dem Wertquery
am nächsten ist. - Schätzen Sie die Wahrscheinlichkeit, dass zwei gleichverteilte Zufallszahlen zwischen 0 und 1 einen Abstand von weniger als 0.1 haben.
- Schreiben Sie eine Funktion
random_walk(n: int) -> np.ndarray
, die eine eindimensionale Zufallsbewegung mitn
Schritten simuliert. Dabei ist jeder Schritt gleichförmig zwischen -1 und 1. - Schreiben Sie eine Funktion, die den Rang einer Matrix errechnet.
- 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. - 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. - 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.
- Schreiben Sie eine Funktion
sort_by_column(a: np.ndarray, column: int) -> np.ndarray
, die eine Matrix nach einer Spalte sortiert. - 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. - 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
- Benutzen Sie
np.einsum
, um das Matrixprodukt@
, das Skalarproduktnp.dot
und das äußere Produktnp.outer
und die Spurnp.trace
zu berechnen. - 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. - 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.
- Finden Sie einen Weg, um mittels
np.random.uniform
das Volumen einer 3D-Kugel mit Radiusr
zu berechnen. Finden Sie dann einen Weg, dieses Problem nur mit 1D-Arrays zu lösen. - 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 Stellei
den Koeffizienten vonx**i
darstellt.
Anwendungsaufgabe für Alle
Wir wollen eine Partikelsimulation schreiben, die die Bewegung von Teilchen in 2D berechnet. Das erfolgt in Schritten:
- Schreiben Sie eine Funktion
init_universe(n: int) -> tuple[np.ndarray, np.ndarray]
, die zufällige Positionen und Geschwindigkeiten fürn
Teilchen generiert. - 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 Zeitspannedt
berechnet. Dabei dürfen die Grenzen des Universums nicht verlassen werden.bounds
gibt zusammen mit dem Ursprung die Grenzen des Universums. -
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")
Hier ist die rote Box die Grenze des Universums. Wenn Sie alles richtig gemacht haben, bleiben die Teilchen innerhalb der Box.
-
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. - 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. - Aktualisieren Sie die Funktion
build_trajectory
so, dass die Geschwindigkeiten der Teilchen berücksichtigt werden. - Finden Sie passende Parameter, die die Teilchen einander umkreisen lassen.
- 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 Argumentforce_callable
, mit dem eine Funktion übergeben wird, die die Kräfte berechnet. Deren Signatur istforce_callable(positions: np.ndarray, masses: np.ndarray) -> np.ndarray
. -
Jetzt wollen wir der Simulation genauer auf die Finger schauen. Ergänzen Sie die Funktion
build_trajectory
um ein Argumentobservers
, 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 Funktionbuild_trajectory
gibt nun ein Dictionary zurück das inpositions
die Koordinaten der Trajektorie enthält und ansonsten in jedem Schlüssel die Zeitreihe des jeweiligen Eintrags inobservers
. Jede Funktion inobservers
hat die Signaturobserver(positions: np.ndarray, masses: np.ndarray, velocities: np.ndarray) -> np.ndarray
. Schreiben Sie Funktionen fürobservers
, 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")
-
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