--- marp: true paginate: true # class: invert theme: rose-pine footer: Tutorium 12 - 19.01.2024 - Nils Pukropp - https://s.narl.io/s/tutorium-12 header: math: mathjax --- # Tutorium 12 - 19.01.2024 Musterlösung 11 - Wiederholung Types - Functions! --- # Musterlösung - Exercise 11 --- # Aufgabe 11.1 - Generatoren; `generators.py` [`10p`] --- ## Aufgabe 11.1 a - collatz; [`2.5p`] Es seien $i \in \mathbb{N}_0$ und $n \in \mathbb{N}$, so ist die Collatz-Folge definiert als $$ \begin{align*} c_0 &= n \\ c_{i+1} &= \begin{cases} \frac{c_i}{2}, &c_i\mod 2 = 0 \\ 3 \cdot c_i + 1, &c_i\mod 2 = 1 \end{cases} \end{align*} $$ Dabei gilt $c_i = 1$ als Abbruchbedingung des Generators --- ## Aufgabe 11.1 a - collatz; [`2.5p`] ```python def collatz(n: int) -> Generator[int, None, None]: if n < 1: return while n > 1: yield n if n % 2 == 0: n = n // 2 else: n = 3 * n + 1 yield n ``` --- ## Aufgabe 11.1 b - random; [`2.5p`] --- ## Aufgabe 11.1 b - random; [`2.5p`] ```python def random(seed: int, a: int, b: int, m: int) -> Iterator[int]: yi = seed while True: yield yi yi = (a * yi + b) % m ``` --- ## Aufgabe 11.1 c - chunks; [`2.5p`] --- ## Aufgabe 11.1 c - chunks; [`2.5p`] ```python def chunks[T](iter: Iterator[T], n: int) -> Iterator[list[T]]: while True: xs = [] try: for _ in range(n): xs.append(next(iter)) yield xs except StopIteration: if xs: yield xs break ``` --- ## Aufgabe 11.1 d - flatten; [`2.5p`] --- ## Aufgabe 11.1 d - flatten; [`2.5p`] ```python def flatten[T](iters: Iterator[list[T]]) -> Iterator[T]: for iter in iters: yield from iter ``` --- # Aufgabe 11.2 - Graphen; `graphs.py` [`10p`] Typaliase als Hilfestellung ```python type GDict[T] = dict[T, set[T]] type Graph[T] = GDict[T] ``` --- ## Aufgabe 11.2 a - is_graph; [`2.5p`] --- ## Aufgabe 11.2 a - is_graph; [`2.5p`] ```python def is_graph(d: GDict[Any]) -> bool: for vals in d.values(): for val in vals: if val not in d.keys(): return False return True ``` --- ## Aufgabe 11.2 b - to_graph; [`2.5p`] --- ## Aufgabe 11.2 b - to_graph; [`2.5p`] ```python def to_graph[T](d: GDict[T]) -> Graph[T]: res = dict() for k, vals in d.items(): for val in vals: if val not in d: res[val] = set() res[k] = vals return res ``` --- ## Aufgabe 11.2 c - nodes, edges; [`2.5p`] --- ## Aufgabe 11.2 c - nodes, edges; [`2.5p`] ```python def edges[T](graph: Graph[T]) -> Iterator[tuple[T, T]]: for key, value in graph.items(): for v in value: yield (key, v) def nodes[T](graph: Graph[T]) -> Iterator[T]: yield from graph.keys() ``` --- ## Aufgabe 11.2 d - invert_graph; [`2.5p`] --- ## Aufgabe 11.2 d - invert_graph; [`2.5p`] ```python def invert_graph[T](graph: Graph[T]) -> Graph[T]: res = dict() for n in nodes(graph): res[n] = set() for a, b in edges(graph): res[b].add(a) return res ``` --- ## Aufgabe 11.2 e - has_cycle; [`0p`] --- ## Aufgabe 11.2 e - has_cycle; [`0p`] ```python def find_cycle[T](graph: Graph[T], start: T, visited: set[T]) -> bool: assert start in graph if start in visited: return True for value in graph[start]: if find_cycle(graph, value, visited | {start}): return True return False def has_cycle(graph: Graph[Any]) -> bool: return any(find_cycle(graph, node, set()) for node in graph) ``` --- ## Aufgabe 11.3 - Erfahrungen `NOTES.md`; [`0p`] ### Tragt eure Stunden ein! --- # Type annotations (Wiederholung) --- ## Type annotations - Was ist das? --- ## Type annotations - Was ist das? * Jedes **Objekt** lässt sich mindestens einem **Typ** zuordnen * Objekte im mathematischen Sinne wie z.B. Variablen, Funktionen, ... * Dieser **schränkt** den Wertebereich ein * z.B. ist eine Variable `x` von Typ `int` eine Ganzzahl * ähnlich zur mathematischen Schreibweise $x \in \mathbb{Z}$ * In der Informatik nennt man das **Typisierung** * Es gibt verschiedene Arten der Typisierung --- ## Type annotations - Typisierung - **dynamische Typisierung** überprüft die gegebenen Typen zur **Laufzeit** - also erst wenn das Programm *läuft* - **statische Typisierung** überprüft die gegebenen Typen zur **Übersetzungszeit** - also während wir den Quellcode übersetzen ### Was ist nun Python? --- ## Type annotations - Typisierung - **dynamische Typisierung** überprüft die gegebenen Typen zur **Laufzeit** - also erst wenn das Programm *läuft* - **statische Typisierung** überprüft die gegebenen Typen zur **Übersetzungszeit** - also während wir den Quellcode übersetzen ### Was ist nun Python? - **dynamisch typisiert** - wir müssen unsere `.py` Datei ausführen bevor wir wissen ob alles korrekt ist - **Pylance** ist ein eigenes Programm - es soll beim Schreiben bereits **Typverletzungen** erkennen - **unvollständige** Typüberprüfung, soll nur den Programmierer unterstützen --- ## Variabeln Typannotieren * `variable_name: = ...` * Beispiele: ```python x: int = 3 y: int = 5 string: str = "Hello World!" # aber auch eigene Objekte (OOP) point: Point = Point(3, 1) ``` * diese Annotation ist für uns **optional** --- ## Funktionen Typannotieren * `def func_name(param1: , param2: , ...) -> ` * Beispiele: ```python def add(x: int, y: int) -> int: return x + y def div(x: float, y: float) -> Optional[float]: if y == 0.0: return None return x / y ``` * diese Annotation ist **verpflichtend** und muss so vollständig wie möglich sein --- ## Klassen Typannotieren * ``` class ClassName: attribute_name1: attribute_name2: ... ``` * Beispiel: ```python @dataclass class Point: x: int y: int ``` * diese Annotation ist **verpflichtend** und muss so vollständig wie möglich sein --- ## Methoden Typannotieren * `def method_name(self, param1: , ...) -> ` * Beispiel: ```python class Point: x: int y: int def distance_from(self, other: 'Point') -> float: return math.sqrt((other.x - self.x) ** 2 + (other.y - self.y) ** 2) ``` * `self` muss **nicht** Typannotiert werden, kann aber * `other` hingegen schon, wegen Python muss in der Klasse mit `'` annotiert werden * diese Annotation ist **verpflichtend** --- ## Datentypen von Datentypen * Manche Datentypen bauen sich aus anderen Datentypen auf * z.B. `list` ist eine Liste von Elementen mit einem Typ * hierfür verwenden wir `[]` um den Datentyp in `list` zu annotieren ```python def sum(xs: list[int]) -> int: total: int = 0 for x in xs: total += x return total ``` * hierbei ist es wichtig so genau wie möglich zu annotieren! * diese Annotation ist **verpflichtend** --- ## Häufige Fehler mit verschachtelten Typen --- ## Fehlerquelle - `tuple[...]` * Tuple haben eine feste größe * Tuple sind endlich * Tuple können Elemente mit unterschiedlichen Typen haben * Die Datentypen der Elemente werden mit einem `,` in `[]` getrennt * Beispiel: ```python tup: tuple[int, int, float, str] = (1, 2, 3.0, "hello world") ``` * Diese Annotation ist **verpflichtend** --- ## Fehlerquelle - `dict[...]` * Dictionary haben genau zwei zu definierende Typen * **Key** * **Value** * Beispiel: ```python number_dictionary: dict[int, str] = { 0: "zero", 1: "one", 2: "two", } ``` * Diese Annotation ist **verpflichtend** * Diese kann weiter geschachtelt werden durch z.B. `list` als `Value`: * `dict[int, list[str]]` --- ## Fehlerquelle - Typvariabeln (generische Typen) * manchmal wollen wir nicht genau wissen welchen Datentypen wir haben * dieser wird dann implizit von Python erkannt * wir stellen damit sicher dass eine Typvariable **beliebig** aber **fest** ist * Beispiel: ```python def add[T](x: T, y: T) -> T: return x + y ``` * `T` kann nur ein Datentyp sein, also muss `type(x) == type(y)` gelten * **außer** wir schrenken `T` mit `|` ein: `T: (int | str)` damit müssen x und y nicht den gleichen Datentypen haben * `T` lässt sich weiter einschränken durch `T: (int, str)`, hierbei ist `T` entweder ein `int` oder (exklusiv) `str` --- ## Fehlerquelle - Was ist TypeVar? * `TypeVar` ist aus früheren Python-Versionen * Typvariablen wurden vor der Python 3.12 so definiert: ```python T = TypeVar('T') ``` * sieht dumm aus, ist es auch, benutzt es nicht! --- ## Fragen zu Typannotationen? --- # Funktionale Programmierung --- ## Funktionale Programmierung - was ist das? - Funktionen sind äquivalent zu Datenobjekten - anonyme Funktionen aka Lambdas - Closures - Programmablauf mit Verkettung und Komposition von Funktionen --- ## Funktionen sind Datenobjekte - Jede Funktion hat den Datentyp `Callable` - Wir können Funktionen wie alle anderen Objekte variabeln zuweisen ```python def add(a: int, b: int) -> int: return a + b add_but_variable = add print(add_but_variable(3, 2)) # 5 ``` --- ## Anonyme Funktionen - `lambda` - Mit dem `lambda` Keyword lassen sich anonyme Funktionen definieren ohne `def` - Bietet sich vor allem an für kleine Funktionen und Kompositionen von Funktionen ```python print(reduce(lambda x, y: x + y, [1, 2, 3, 4])) # 10 ``` - hat als Datentyp auch `Callable` ```python add: Callable[[int, int], int] = lambda x, y: x + y ``` --- ## Closures - Verkettete Funktionen, bei denen die Variabeln aus vorherigen benutzt werden können ```python def poly(x: float) -> Callable[[float, float], Callable[[float], float]]: return lambda a, b: lambda c: a * x ** 2 + b * x + c print(poly(3)(2, 3)(5)) # 2 * 3 ** 2 + 3 * 3 + 5 = 32 ``` - kein wirklich schönes Beispiel, ein besseres ist `compose` für Kompositionen --- ## Komposition - Verketten von Funktionen ```python def compose[T](*funcs: Callable[[T], T]) -> Callable[[T], T]: return fold(lambda f, g: lambda n: f(g(n)), funcs) f: Callable[[int], int] = lambda n: n + 42 g: Callable[[int], int] = lambda n: n ** 2 h: Callable[[int], int] = lambda n: n - 3 print(compose(f, g, h)(0)) ``` --- ## Higher-Order Functions - nehmen eine oder mehrere `Callable` als Argument - geben ein `Callable` zurück ### Higher-Order-Function - `map` - Wendet ein `Callable` auf jedes Element in einem `Iterable` an ```python def map[T, R](func: Callable[[T], R], xs: Iterable[T]) -> Iterable[R]: return [func(x) for x in xs] numeric_list = list(map(lambda e: int(e), ['1', '2', '3'])) print(numeric_list) # [1, 2, 3] ``` --- ### Higher-Order-Function - `filter` - `filter` verarbeitet Datenstrukturen anhand eines Prädikats (`Callable`) - behält nur Elemente die das Prädikat erfüllen ```python def filter[T](predicate: Callable[[T], bool], xs: Iterable[T]) -> Iterable[T]: return [x for x in xs if predicate(x)] predicate: Callable[[int | None] bool] = lambda e: bool(e) none_free_list: list[int] = list(filter(predicate, [1, 2, 3, None, 5, 6])) print(none_free_list) # [1, 2, 3, 5, 6] - kein None ``` --- ### Higher-Order-Function - `fold` - Kombiniert Elemente einer Datenstruktur ```python def fold[T](func: Callable[[T, T], T], xs: Iterable[T]) -> T: it: Iterator[T] = iter(xs) value: T | None = None for x in it: match value: case None: value = x case _: value = func(value, x) if not value: raise TypeError("can't fold empty list") return value sum: Callable[[Iterable[int]], int] = lambda xs: fold(lambda x, y: x + y, xs) print(sum([1, 2, 3, 4])) # 10 ``` --- ### keine Higher-Order-Function - `flatten` - Nimmt mehrdimensionale Listen und macht eine Liste draus ```python def flatten(xs: Iterable[Any]) -> Iterable[Any]: new_list = [] for s in xs: if isinstance(s, Iterable): new_list += flatten(s) else: new_list.append(s) return new_list flattened = list(flatten([[1, 2, 3], 4, [[5, 6], 7, [8, 9]]])) print(flattened) # [1, 2, 3, 4, 5, 6, 7, 8, 9] ``` - nimmt weder `Callable` als Argumente - gibt kein `Callable` zurück - ist keine Higher-Order-Function --- # Fragen zur funktionalen Programmierung? --- # Weitere allgemeine Fragen?