Aufwärmen · Aufgabe 1 von 7
// A2.2 · ca. 30 Min. · Vertiefung
Generics mit der Syntax aus PEP 695
Nach dieser Lektion schreiben Sie generische Funktionen und Klassen mit def f[T] und class Box[T], schränken einen Typparameter mit einem Bound oder Constraints ein und definieren Typ-Aliase mit der type-Anweisung.
Danach können Sie
- Generische Funktionen und Klassen mit def f[T] und class Box[T] schreiben
- Einen Typparameter mit Bound oder Constraints einschränken und type-var-Fehler von mypy lesen
- Typ-Aliase mit der type-Anweisung definieren und ihr Verhalten zur Laufzeit vorhersagen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen: Was gibt das aus?
class Box[T]: def __init__(self, item: T) -> None: self.item = item box = Box[int]("seven") print(box.item, type(box).__name__)Üben · Aufgabe 3 von 7
Machen Sie first generisch: Deklarieren Sie den Typparameter T, damit das Ergebnis den Typ der Listenelemente hat.
def first____(items: list[T]) -> T:def first(items: list[T]) -> T:Üben · Aufgabe 4 von 7
reveal_type() bittet mypy, den abgeleiteten Typ auszugeben. Welchen Typ zeigt mypy für die letzte Zeile?
def first[T](items: list[T]) -> T: return items[0] reveal_type(first(["a", "b"]))Üben · Aufgabe 5 von 7
Ordnen Sie jeder Syntax zu, was sie deklariert.
Denksport · Aufgabe 6 von 7
Knobelaufgabe. Der Alias nennt Point, bevor es die Klasse gibt. Was gibt das aus?
type Pair = tuple[Point, Point] class Point: pass print(Pair.__value__)Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Schreiben Sie eine generische Funktion chunks(items, size), die eine Liste in Listen mit höchstens size Elementen teilt: chunks([1, 2, 3, 4, 5], 2) ergibt [[1, 2], [3, 4], [5]]. Annotieren Sie sie mit einem Typparameter, damit mypy weiß, dass chunks einer list[str] eine list[list[str]] ist.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Ein generischer Stack, ein Bound und ein generischer Alias
Stack[T] enthält Elemente eines Typs; Stack[str]() legt T auf str fest, also würde mypy names.push(3) ablehnen. longest nimmt alles mit einer Länge, weil S an Sized gebunden ist. Pair[T] ist ein generischer Alias, verwendet in den Hinweisen von swap. Die letzten Zeilen zeigen, was von alldem zur Laufzeit existiert.
main.py
from collections.abc import Sized
class Stack[T]:
"""A last-in, first-out stack of items of one type."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def __len__(self) -> int:
return len(self._items)
def longest[S: Sized](*items: S) -> S:
return max(items, key=len)
type Pair[T] = tuple[T, T]
def swap[T](pair: Pair[T]) -> Pair[T]:
return pair[1], pair[0]
names = Stack[str]()
names.push("Ada")
names.push("Grace")
print(names.pop(), len(names))
print(longest("tea", "coffee", "milk"), longest([1, 2], [3]))
print(swap((1, 2)), Pair[int], Pair.__value__)
print(Stack.__type_params__, longest.__type_params__[0].__bound__)
Ausführen mit
python main.pyAusgabe
Grace 1
coffee [1, 2]
(2, 1) Pair[int] tuple[T, T]
(T,) <class 'collections.abc.Sized'>- self._items: list[T] verwendet das T der Klasse in einer Methode, wo es sichtbar ist.
- longest funktioniert für Strings wie für Listen; longest(3, 4) würde mypy ablehnen, weil int kein len() hat.
- Pair[int] erscheint als Pair[int]: Der Alias behält seinen Namen. __value__ zeigt, wofür er steht.
- __type_params__ und __bound__ sind alles, was die Klammern zur Laufzeit hinterlassen.
Ändern und ausführen
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übungen
Übung 1 von 2
Generische Hilfsfunktionen
Machen Sie beide Funktionen generisch. first_or(items, default) liefert das erste Element oder den Standardwert; mit einem Typparameter T nimmt sie list[T] und ein T und liefert ein T. longest(items) liefert das erste längste Element oder None bei leerer Liste; geben Sie ihr einen an Sized gebundenen Typparameter S, nehmen Sie list[S] und liefern Sie S | None.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Deklarieren Sie den Parameter nach dem Namen: def first_or[T](items: list[T], default: T) -> T:.
Hinweis 2
Ein Bound steht nach einem Doppelpunkt in den Klammern: def longest[S: Sized](...).
Hinweis 3
Der Rückgabe-Hinweis von longest ist S | None: der Elementtyp selbst oder None.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from collections.abc import Sized
def first_or[T](items: list[T], default: T) -> T:
return items[0] if items else default
def longest[S: Sized](items: list[S]) -> S | None:
if not items:
return None
return max(items, key=len)
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
from collections.abc import Sized
def first_or(items: list[object], default: object) -> object:
return items[0] if items else default
def longest(items: list[Sized]) -> Sized | None:
if not items:
return None
return max(items, key=len)
test_main.py
from collections.abc import Sized
from typing import get_type_hints
from main import first_or, longest
def test_first_or_is_generic():
"""first_or hat einen Typparameter T, der in allen Hinweisen steht"""
params = first_or.__type_params__
assert len(params) == 1, f"first_or.__type_params__ ist {params!r}, erwartet war ein Parameter"
t = params[0]
got = get_type_hints(first_or)
assert got == {"items": list[t], "default": t, "return": t}, f"die Hinweise von first_or sind {got!r}"
def test_first_or_behaviour():
"""first_or liefert das erste Element oder bei leerer Liste den Standardwert"""
got = first_or([3, 4], 0), first_or([], "none")
assert got == (3, "none"), f"first_or lieferte {got!r}, erwartet war (3, 'none')"
def test_longest_bound():
"""longest hat einen Typparameter, an Sized gebunden"""
params = longest.__type_params__
assert len(params) == 1 and params[0].__bound__ is Sized, f"longest.__type_params__ ist {params!r}; erwartet war ein an Sized gebundener Parameter"
s = params[0]
got = get_type_hints(longest)
assert got == {"items": list[s], "return": s | None}, f"die Hinweise von longest sind {got!r}"
def test_longest_behaviour():
"""longest liefert das erste längste Element oder None bei leerer Liste"""
got = longest(["tea", "coffee", "cocoa!"]), longest([{1}, {2, 3}]), longest([])
assert got == ("coffee", {2, 3}, None), f"longest lieferte {got!r}"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
Eine generische Warteschlange mit Batch-Alias
Machen Sie Queue zu einer generischen Klasse Queue[T]: put nimmt ein T, get liefert ein T. Fügen Sie mit der type-Anweisung einen generischen Alias Batch[T] für list[T] hinzu und implementieren Sie take(n): Es entfernt bis zu n Elemente von vorn und liefert sie, annotiert mit Rückgabe Batch[T]. take scheitert nie, wenn weniger Elemente übrig sind.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Der Alias bekommt einen eigenen Parameter: type Batch[T] = list[T]. Auch die Klasse deklariert ihr eigenes T: class Queue[T]:.
Hinweis 2
Ersetzen Sie object durch T in der Liste sowie in put und get, und annotieren Sie take mit -> Batch[T].
Hinweis 3
Slicing scheitert nie bei kurzen Listen: taken = self._items[:n], danach del self._items[:n].
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
type Batch[T] = list[T]
class Queue[T]:
"""First in, first out."""
def __init__(self) -> None:
self._items: list[T] = []
def put(self, item: T) -> None:
self._items.append(item)
def get(self) -> T:
return self._items.pop(0)
def take(self, n: int) -> Batch[T]:
taken = self._items[:n]
del self._items[:n]
return taken
def __len__(self) -> int:
return len(self._items)
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
# Add the alias here: Batch[T] stands for list[T].
class Queue:
"""First in, first out."""
def __init__(self) -> None:
self._items: list[object] = []
def put(self, item: object) -> None:
self._items.append(item)
def get(self) -> object:
return self._items.pop(0)
def take(self, n: int) -> list[object]:
# Remove up to n items from the front and return them.
return []
def __len__(self) -> int:
return len(self._items)
test_main.py
from typing import TypeAliasType
import main
from main import Queue
def test_batch_alias():
"""Batch ist ein generischer Typ-Alias für list[T]"""
batch = getattr(main, "Batch", None)
assert isinstance(batch, TypeAliasType), f"main.Batch ist {batch!r}, erwartet war ein Alias aus einer type-Anweisung"
assert len(batch.__type_params__) == 1, f"Batch hat die Typparameter {batch.__type_params__!r}, erwartet war einer"
t = batch.__type_params__[0]
assert batch.__value__ == list[t], f"Batch steht für {batch.__value__!r}, erwartet war list[T]"
def test_queue_is_generic():
"""Queue hat einen Typparameter, und take() liefert Batch[T]"""
params = Queue.__type_params__
assert len(params) == 1, f"Queue.__type_params__ ist {params!r}, erwartet war ein Parameter"
got = Queue.take.__annotations__["return"]
assert got == main.Batch[params[0]], f"take() ist mit Rückgabe {got!r} annotiert, erwartet war Batch[T]"
def test_first_in_first_out():
"""get() liefert die Elemente in der Reihenfolge, in der sie hineinkamen"""
queue = Queue[str]()
for word in ["a", "b", "c"]:
queue.put(word)
got = queue.get(), queue.get(), len(queue)
assert got == ("a", "b", 1), f"zwei get()-Aufrufe und len() ergaben {got!r}, erwartet war ('a', 'b', 1)"
def test_take():
"""take(2) entfernt die zwei ältesten Elemente, take(5) das übrige"""
queue = Queue[int]()
for n in [1, 2, 3]:
queue.put(n)
got = queue.take(2), queue.take(5), len(queue)
assert got == ([1, 2], [3], 0), f"take(2), take(5) und len() ergaben {got!r}, erwartet war ([1, 2], [3], 0)"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHäufige Fehler
isinstance mit einem Typparameter
def keep[T](items: list[object], kind: T) -> list[object]:
return [item for item in items if isinstance(item, T)]
print(keep([1, "a", 2], int))
Was Python ausgibt
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a unionWarum, und die Lösung
Zur Laufzeit ist T ein typing.TypeVar, ein Platzhalter für den Prüfer, nicht die übergebene Klasse. Übergeben Sie die Klasse selbst und annotieren Sie sie so: def keep[T](items: list[object], kind: type[T]) -> list[T]:, dann prüfen Sie isinstance(item, kind).
Einen Typ-Alias wie eine Klasse aufrufen
type Grid = list[list[int]]
grid = Grid()
Was Python ausgibt
TypeError: 'typing.TypeAliasType' object is not callableWarum, und die Lösung
Eine type-Anweisung erzeugt ein TypeAliasType für Annotationen, keine Klasse zum Instanziieren. Bauen Sie den Wert mit dem echten Typ und nutzen Sie den Alias im Hinweis: grid: Grid = []. Wenn Sie einen aufrufbaren Namen brauchen, ist eine einfache Zuweisung wie Grid = list etwas anderes, ohne verzögerte Auswertung.
Die neue Syntax mit Generic[T] mischen
from typing import Generic, TypeVar
T = TypeVar("T")
class Box[T](Generic[T]):
pass
Was Python ausgibt
TypeError: Cannot inherit from Generic[...] multiple times.Warum, und die Lösung
class Box[T]: fügt Generic schon als Basisklasse hinzu, ein weiteres Generic[T] fügt es ein zweites Mal hinzu. Bleiben Sie bei einem Stil: class Box[T]: ganz ohne TypeVar, oder das ältere T = TypeVar("T") mit class Box(Generic[T]):. Neuer Code ab Python 3.12 verwendet die Klammern.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.