.vscode
Tutorium
tut01
tut02
tut03
tut04
tut05
tut06
tut07
tut08
tut09
tut10
tut11
tut12
src
README.md
notes.md
slides.pdf
tut13
tut14
src
.gitignore
LICENSE
README.md
marp, paginate, theme, footer, header, math
marp | paginate | theme | footer | header | math |
---|---|---|---|---|---|
true | true | rose-pine | Tutorium 12 - 19.01.2024 - Nils Pukropp - https://s.narl.io/s/tutorium-12 | 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
]
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
]
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
]
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
]
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
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
]
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
]
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
]
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
]
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
]
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 Typint
eine Ganzzahl - ähnlich zur mathematischen Schreibweise
x \in \mathbb{Z}
- z.B. ist eine Variable
- 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
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?
Was ist nun Python?
- dynamisch typisiert
- wir müssen unsere
.py
Datei ausführen bevor wir wissen ob alles korrekt ist
- wir müssen unsere
- Pylance ist ein eigenes Programm
- es soll beim Schreiben bereits Typverletzungen erkennen
- unvollständige Typüberprüfung, es soll nur den Entwickler unterstützen
Variabeln Typannotieren
variable_name: <Type> = ...
- Beispiele:
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: <Type>, param2: <Type>, ...) -> <Type>
- Beispiele:
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: <Type> attribute_name2: <Type> ...
- Beispiel:
@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: <Type>, ...) -> <Type>
- Beispiel:
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 aberother
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 inlist
zu annotierendef 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:
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:
number_dictionary: dict[int, str] = { 0: "zero", 1: "one", 2: "two", }
- Diese Annotation ist verpflichtend
- Diese kann weiter geschachtelt werden durch z.B.
list
alsValue
: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:
def add[T](x: T, y: T) -> T: return x + y
T
kann nur ein Datentyp sein, also musstype(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 durchT: (int, str)
, hierbei istT
entweder einint
oder (exklusiv)str
Fehlerquelle - Was ist TypeVar?
TypeVar
ist aus früheren Python-Versionen- Typvariablen wurden vor der Python 3.12 so definiert:
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
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 ohnedef
- Bietet sich vor allem an für kleine Funktionen und Kompositionen von Funktionen
print(reduce(lambda x, y: x + y, [1, 2, 3, 4])) # 10
- hat als Datentyp auch
Callable
add: Callable[[int, int], int] = lambda x, y: x + y
Closures
- Verkettete Funktionen, bei denen die Variabeln aus vorherigen benutzt werden können
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
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 einemIterable
andef 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
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: e is not None 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
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
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