Zum Inhalt springen
aviral gupta

// 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.

Lektion 2 von 6 in A2 Typisierung

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
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Lektion A2.1: first(items: list[object]) -> object liefert ein Element der Liste. Was dürfen Sie aus Sicht von mypy ohne Prüfung mit dem Ergebnis tun?

  2. 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__)
  3. Ü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:
  4. Ü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"]))
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jeder Syntax zu, was sie deklariert.

  6. 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__)
  7. 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.py

Ausgabe

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
  1. Hinweis 1

    Deklarieren Sie den Parameter nach dem Namen: def first_or[T](items: list[T], default: T) -> T:.

  2. Hinweis 2

    Ein Bound steht nach einem Doppelpunkt in den Klammern: def longest[S: Sized](...).

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.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
  1. Hinweis 1

    Der Alias bekommt einen eigenen Parameter: type Batch[T] = list[T]. Auch die Klasse deklariert ihr eigenes T: class Queue[T]:.

  2. Hinweis 2

    Ersetzen Sie object durch T in der Liste sowie in put und get, und annotieren Sie take mit -> Batch[T].

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Hä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 union

Warum, 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 callable

Warum, 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.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Typparameter verbindet Typen

def first[T](items: list[T]) -> T: deklariert in Klammern nach dem Namen einen Typparameter T. T steht pro Aufruf für einen Typ: Für first([1, 2]) leitet mypy int ab, für first(["a"]) str. class Stack[T]: macht dasselbe für eine Klasse, und jede Methode kann T verwenden; Stack[str]() legt ihn fest, bei Stack(3) leitet mypy ihn ab. T existiert nur in der Funktion oder Klasse: Außerhalb ist der Name nicht definiert. Die Syntax erzeugt TypeVar und Generic-Basis, die älterer Code von Hand schrieb.

Bounds und Constraints

Ein bloßes T kann nur, was jedes Objekt kann, also ist a > b auf zwei T-Werten für mypy ein Fehler. Ein Bound, [S: Sized], erlaubt jeden Untertyp von Sized, und im Rumpf dürfen Sie len() aufrufen. Constraints, [N: (int, float)], erlauben pro Aufruf genau einen der genannten Typen. Ein Aufruf, der eine der Regeln bricht, bekommt mypys Fehler [type-var]: Value of type variable "S" of "longest" cannot be "int". Zur Laufzeit prüft Python nichts davon: Box[int]("seven") läuft.

Typ-Aliase mit der type-Anweisung

type Pair = tuple[int, int] gibt einem Typ einen Namen. Die Anweisung erzeugt ein typing.TypeAliasType, dessen Wert erst ausgewertet wird, wenn Sie Pair.__value__ lesen; der Wert darf also Klassen nennen, die weiter unten definiert sind. Auch ein Alias kann Parameter haben: type Pair[T] = tuple[T, T], verwendet als Pair[str]. Ein Alias ist für Annotationen gedacht, er ist keine Klasse: isinstance(x, Pair) löst TypeError aus, und Pair() ist nicht aufrufbar.

Quellen

Zuletzt geprüft am 29. September 2026