Zum Inhalt springen
aviral gupta

// A1.6 · ca. 40 Min. · Vertiefung

Build: Attribut-Hooks, Klassenerzeugung, Vector und Matrix

Nach dieser Lektion greifen Sie in gescheiterte und in alle Attributzugriffe ein, führen Code für jede neue Unterklasse aus, erkennen, wann eine Metaklasse passt, und haben Vector und Matrix mit Operatoren gebaut.

Lektion 6 von 6 in A1 Das Datenmodell

Ende des Moduls

Danach können Sie

  • Zwischen __getattr__ und __getattribute__ wählen und beide ohne Rekursion schreiben
  • __init_subclass__ mit Klassen-Schlüsselwörtern nutzen, um Unterklassen zu prüfen oder zu registrieren
  • Erklären, was eine Metaklasse ist und wann sie läuft, im Vergleich zu __init_subclass__
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Lektion A1.5: Ein Eintrag im Instanz-Dictionary hat denselben Namen wie ein Nicht-Daten-Deskriptor der Klasse. Was liefert obj.name?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen: Die Klasse definiert __getattr__. Was gibt das aus?

    class Fallback:
        color = "red"
    
        def __getattr__(self, name):
            return f"<{name} missing>"
    
    
    f = Fallback()
    print(f.color, f.size)
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Hook, sodass fehlende Attribute "default" liefern, vorhandene aber ihren Wert behalten.

    class Defaults:
        known = "set"
    
        def ____(self, name):
            return "default"
    def (self, name):
  4. Üben · Aufgabe 4 von 7

    Counter überschreibt __getattribute__. Was gibt das aus?

    class Counter:
        def __init__(self):
            self.value = 1
    
        def __getattribute__(self, name):
            print("get", name, end="; ")
            return object.__getattribute__(self, name)
    
    
    c = Counter()
    print(c.value)
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Hook zu, wann Python ihn aufruft.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. B erbt von A, A erbt von Base. Was gibt das aus?

    class Base:
        count = 0
    
        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            Base.count += 1
    
    
    class A(Base):
        pass
    
    
    class B(A):
        pass
    
    
    print(Base.count)
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie eine Basisklasse Shape, die jede Unterklasse prüft, sobald deren class-Anweisung läuft: Eine Unterklasse ohne Methode area löst TypeError("<Name> must define area()") aus, und der Name jeder akzeptierten Unterklasse kommt in Shape.registry.

    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

Datensätze mit Feldern aus der Klassenzeile

Record nutzt zwei Hooks. __init_subclass__ liest das Schlüsselwort fields aus jeder Klassenzeile und registriert die Unterklasse. __getattr__ macht die gespeicherten Werte zu Attributen und läuft nur, wenn die normale Suche scheitert; book.fields kommt also weiter aus der Klasse. Fügen Sie eine Unterklasse Movie hinzu, oder lesen Sie book._data, bevor __init__ gelaufen ist.

main.py

class Record:
    """Base class: each subclass names its fields in the class line."""

    fields: tuple[str, ...] = ()
    kinds: dict[str, type["Record"]] = {}

    def __init_subclass__(cls, /, fields: str = "", **kwargs: object) -> None:
        super().__init_subclass__(**kwargs)
        cls.fields = tuple(fields.split())
        Record.kinds[cls.__name__.lower()] = cls

    def __init__(self, *values: object) -> None:
        if len(values) != len(self.fields):
            raise TypeError(f"{type(self).__name__} needs {len(self.fields)} values")
        self._data = dict(zip(self.fields, values))

    def __getattr__(self, name: str) -> object:
        # Only called when the normal lookup has failed.
        if name.startswith("_"):
            raise AttributeError(name)  # never look up _data through itself
        try:
            return self._data[name]
        except KeyError:
            raise AttributeError(f"{type(self).__name__!r} has no field {name!r}") from None


class Book(Record, fields="title author"):
    pass


class Song(Record, fields="title artist year"):
    pass


print(Record.kinds)
book = Book("Dune", "Herbert")
print(book.title, book.author, book.fields)
try:
    book.year
except AttributeError as err:
    print(err)
print(type(Book).__name__)

Ausführen mit

python main.py

Ausgabe

{'book': <class '__main__.Book'>, 'song': <class '__main__.Song'>}
Dune Herbert ('title', 'author')
'Book' has no field 'year'
type
  • fields="title author" ist ein Klassen-Schlüsselwort; Python reicht es an Record.__init_subclass__ weiter.
  • Die Prüfung auf "_" verhindert, dass __getattr__ self._data über sich selbst sucht, wenn _data noch nicht existiert.
  • Der KeyError wird zu AttributeError, also liefert hasattr(book, "year") False.
  • type(Book) ist type: Dafür war keine Metaklasse nötig.
Ä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 3

Schritt 1: ein Vector-Container mit benannten Komponenten

Modul-Build, Schritt 1 von 3. Geben Sie Vector das Container-Protokoll aus A1.1 und A1.3: __repr__ als Vector(3, 4), __len__, __iter__, __getitem__, __eq__ und ein passendes __hash__. Ergänzen Sie dann __getattr__, sodass v.x, v.y und v.z die Komponenten 0 bis 2 lesen, sofern es sie gibt; jeder andere fehlende Name löst AttributeError aus.

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

    Bauen Sie die repr mit ", ".join(map(repr, self._components)), damit eine Komponente Vector(1.5) ergibt.

  2. Hinweis 2

    Hashen Sie dasselbe Tupel, das __eq__ vergleicht: hash(self._components).

  3. Hinweis 3

    In __getattr__: index = self._names.find(name); nur gültig, wenn len(name) == 1 und 0 <= index < len(self._components). Prüfen Sie len(name) zuerst: So rekursiert ein fehlendes _components nie.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from collections.abc import Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


if __name__ == "__main__":
    v = Vector(3, 4)
    print(v, len(v), list(v), v[1])
    print(v.x, v.y, v == Vector(3, 4), {v, Vector(3, 4)})
    try:
        v.z
    except AttributeError as err:
        print(err)
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 Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    # Step 1 of the build. Add:
    # __repr__  -> "Vector(3, 4)"
    # __len__, __iter__ and __getitem__ over the components
    # __eq__ (NotImplemented for non-vectors) and a matching __hash__
    # __getattr__: v.x, v.y, v.z give components 0, 1, 2 if the vector
    #   has that many; any other name raises AttributeError.


if __name__ == "__main__":
    v = Vector(3, 4)
    print(v)

test_main.py

from main import Vector


def test_repr():
    """repr zeigt die Komponenten"""
    assert repr(Vector(3, 4)) == "Vector(3, 4)", f"repr ist {Vector(3, 4)!r}"
    assert repr(Vector(1.5)) == "Vector(1.5)", f"repr eines Vektors mit einer Komponente ist {Vector(1.5)!r}"


def test_sequence_protocol():
    """len, Iteration und Indizierung funktionieren"""
    v = Vector(3, 4, 5)
    got = (len(v), list(v), v[-1])
    assert got == (3, [3, 4, 5], 5), f"(len(v), list(v), v[-1]) ist {got!r}"


def test_equality_and_hash():
    """Gleiche Vektoren sind gleich und haben gleiche Hashes"""
    assert Vector(1, 2) == Vector(1, 2), "Vector(1, 2) != Vector(1, 2)"
    assert Vector(1, 2) != Vector(2, 1), "Vector(1, 2) == Vector(2, 1)"
    assert len({Vector(1, 2), Vector(1, 2)}) == 1, "gleiche Vektoren müssen gleiche Hashes haben"
    assert Vector(1, 2) != (1, 2), "ein Vector sollte nicht gleich einem Tupel sein"


def test_named_components():
    """x, y und z lesen die ersten drei Komponenten"""
    v = Vector(3, 4, 5)
    got = (v.x, v.y, v.z)
    assert got == (3, 4, 5), f"(v.x, v.y, v.z) ist {got!r}"


def test_missing_names_raise():
    """z bei einem 2D-Vektor und unbekannte Namen lösen AttributeError aus"""
    for name in ["z", "w", "xy"]:
        try:
            getattr(Vector(3, 4), name)
        except AttributeError:
            continue
        raise AssertionError(f"Vector(3, 4).{name} sollte AttributeError auslösen")

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 3

Schritt 2: Vektor-Operatoren

Schritt 2 von 3. Ergänzen Sie die Operatoren aus A1.2: v + w, v - w und -v komponentenweise, v * 2 und 2 * v, v @ w als Skalarprodukt und abs(v) als Länge. + und @ lösen bei verschiedenen Längen ValueError("lengths differ: 2 and 3") aus. Jeder Operator gibt für nicht unterstützte Operandentypen NotImplemented zurück.

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

    Eine Hilfsmethode _same_length(other) hält die Längenprüfung an einer Stelle.

  2. Hinweis 2

    v - w kann die anderen nutzen: return self + -other.

  3. Hinweis 3

    Akzeptieren Sie in __mul__ nur int und float, dann schreiben Sie __rmul__ = __mul__. Ohne Typprüfung würde v * "a" still einen Vektor aus Strings bauen.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import math
from collections.abc import Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    def _same_length(self, other: "Vector") -> None:
        if len(self) != len(other):
            raise ValueError(f"lengths differ: {len(self)} and {len(other)}")

    def __add__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return Vector(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        return self + -other

    def __neg__(self) -> "Vector":
        return Vector(*(-a for a in self))

    def __mul__(self, scalar: object) -> "Vector":
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*(a * scalar for a in self))

    __rmul__ = __mul__

    def __matmul__(self, other: object) -> float:
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return sum(a * b for a, b in zip(self, other))

    def __abs__(self) -> float:
        return math.hypot(*self)


if __name__ == "__main__":
    v, w = Vector(3, 4), Vector(1, 2)
    print(v + w, v - w, -w)
    print(v * 2, 2 * v, v @ w, abs(v))
    try:
        v + Vector(1, 2, 3)
    except ValueError as err:
        print(err)
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

import math
from collections.abc import Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    # Step 2 of the build. Add operators; each returns NotImplemented for
    # operands it does not support:
    # v + w, v - w and -v, component by component; + and - raise
    #   ValueError("lengths differ: 2 and 3") for different lengths
    # v * 2 and 2 * v (int or float only)
    # v @ w, the dot product (same length check)
    # abs(v), the length: math.hypot(*v)


if __name__ == "__main__":
    v, w = Vector(3, 4), Vector(1, 2)
    print(v + w, v - w, -w)
    print(v * 2, 2 * v, v @ w, abs(v))
    try:
        v + Vector(1, 2, 3)
    except ValueError as err:
        print(err)

test_main.py

from main import Vector


def test_add_sub_neg():
    """+, - und das unäre - arbeiten komponentenweise"""
    v, w = Vector(3, 4), Vector(1, 2)
    got = (v + w, v - w, -w)
    assert got == (Vector(4, 6), Vector(2, 2), Vector(-1, -2)), f"(v + w, v - w, -w) ist {got!r}"


def test_scalar_on_both_sides():
    """v * 2 und 2 * v ergeben denselben Vektor"""
    v = Vector(1, 2)
    got = (v * 2, 2 * v, 0.5 * v)
    assert got == (Vector(2, 4), Vector(2, 4), Vector(0.5, 1.0)), f"(v * 2, 2 * v, 0.5 * v) ist {got!r}"


def test_dot_and_abs():
    """@ ist das Skalarprodukt und abs() die Länge"""
    dot = Vector(1, 2, 3) @ Vector(4, 5, 6)
    assert dot == 32, f"Vector(1, 2, 3) @ Vector(4, 5, 6) ist {dot!r}, erwartet 32"
    assert abs(Vector(3, 4)) == 5.0, f"abs(Vector(3, 4)) ist {abs(Vector(3, 4))!r}, erwartet 5.0"


def test_length_mismatch():
    """Vektoren verschiedener Länge lösen ValueError aus"""
    for text, op in [("+", lambda a, b: a + b), ("-", lambda a, b: a - b), ("@", lambda a, b: a @ b)]:
        try:
            op(Vector(1, 2), Vector(1, 2, 3))
        except ValueError:
            continue
        raise AssertionError(f"Vector(1, 2) {text} Vector(1, 2, 3) sollte ValueError auslösen")


def test_unsupported_operands():
    """Andere Operandentypen enden über NotImplemented in TypeError"""
    cases = [
        ("Vector(1, 2) + (1, 2)", lambda: Vector(1, 2) + (1, 2)),
        ('Vector(1, 2) * "a"', lambda: Vector(1, 2) * "a"),
        ('"a" * Vector(1, 2)', lambda: "a" * Vector(1, 2)),
        ("Vector(1, 2) @ 3", lambda: Vector(1, 2) @ 3),
    ]
    for text, op in cases:
        try:
            op()
        except TypeError:
            continue
        raise AssertionError(f"{text} sollte TypeError auslösen")

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 3 von 3

Schritt 3: eine Matrix auf Basis von Vector

Schritt 3 von 3. Matrix speichert ihre Zeilen als Vectors und hat bereits shape, T (Transponierte), Indizierung mit m[i, j], repr und Gleichheit. Ergänzen Sie m + n (ValueError bei verschiedenen Formen) und m @ other: mit einem Vector ergibt es den Vector der Zeilen-Skalarprodukte, mit einer Matrix das Matrixprodukt. Für alles andere geben Sie NotImplemented zurück.

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

    Zeilen sind Vectors, also erledigen row @ other und a + b bereits die Rechnung und die Längenprüfung.

  2. Hinweis 2

    Prüfen Sie in __matmul__ isinstance(other, Vector) und isinstance(other, Matrix) getrennt; der Ergebnistyp unterscheidet sich.

  3. Hinweis 3

    Eintrag (i, j) von m @ n ist Zeile i von m @ Spalte j von n. Die Spalten von n sind other.T.rows: Matrix([row @ column for column in columns] for row in self.rows).

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import math
from collections.abc import Iterable, Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    def _same_length(self, other: "Vector") -> None:
        if len(self) != len(other):
            raise ValueError(f"lengths differ: {len(self)} and {len(other)}")

    def __add__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return Vector(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        return self + -other

    def __neg__(self) -> "Vector":
        return Vector(*(-a for a in self))

    def __mul__(self, scalar: object) -> "Vector":
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*(a * scalar for a in self))

    __rmul__ = __mul__

    def __matmul__(self, other: object) -> float:
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return sum(a * b for a, b in zip(self, other))

    def __abs__(self) -> float:
        return math.hypot(*self)



class Matrix:
    """An immutable matrix, stored as a tuple of row vectors."""

    def __init__(self, rows: Iterable[Iterable[float]]) -> None:
        self.rows = tuple(Vector(*row) for row in rows)
        if len({len(row) for row in self.rows}) > 1:
            raise ValueError("all rows must have the same length")

    @property
    def shape(self) -> tuple[int, int]:
        return len(self.rows), len(self.rows[0]) if self.rows else 0

    @property
    def T(self) -> "Matrix":
        return Matrix(zip(*self.rows))

    def __repr__(self) -> str:
        return f"Matrix({[list(row) for row in self.rows]})"

    def __getitem__(self, index: tuple[int, int]) -> float:
        i, j = index
        return self.rows[i][j]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Matrix):
            return NotImplemented
        return self.rows == other.rows

    def __hash__(self) -> int:
        return hash(self.rows)

    def __add__(self, other: object) -> "Matrix":
        if not isinstance(other, Matrix):
            return NotImplemented
        if self.shape != other.shape:
            raise ValueError(f"shapes differ: {self.shape} and {other.shape}")
        return Matrix(a + b for a, b in zip(self.rows, other.rows))

    def __matmul__(self, other: object) -> "Matrix | Vector":
        if isinstance(other, Vector):
            return Vector(*(row @ other for row in self.rows))
        if isinstance(other, Matrix):
            columns = other.T.rows
            return Matrix([row @ column for column in columns] for row in self.rows)
        return NotImplemented


if __name__ == "__main__":
    m = Matrix([[1, 2], [3, 4]])
    identity = Matrix([[1, 0], [0, 1]])
    print(m.shape, m[1, 0], m.T)
    print(m + identity)
    print(m @ Vector(1, 1))
    print(m @ identity == m, m @ m)
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

import math
from collections.abc import Iterable, Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    def _same_length(self, other: "Vector") -> None:
        if len(self) != len(other):
            raise ValueError(f"lengths differ: {len(self)} and {len(other)}")

    def __add__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return Vector(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        return self + -other

    def __neg__(self) -> "Vector":
        return Vector(*(-a for a in self))

    def __mul__(self, scalar: object) -> "Vector":
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*(a * scalar for a in self))

    __rmul__ = __mul__

    def __matmul__(self, other: object) -> float:
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return sum(a * b for a, b in zip(self, other))

    def __abs__(self) -> float:
        return math.hypot(*self)



class Matrix:
    """An immutable matrix, stored as a tuple of row vectors."""

    def __init__(self, rows: Iterable[Iterable[float]]) -> None:
        self.rows = tuple(Vector(*row) for row in rows)
        if len({len(row) for row in self.rows}) > 1:
            raise ValueError("all rows must have the same length")

    @property
    def shape(self) -> tuple[int, int]:
        return len(self.rows), len(self.rows[0]) if self.rows else 0

    @property
    def T(self) -> "Matrix":
        return Matrix(zip(*self.rows))

    def __repr__(self) -> str:
        return f"Matrix({[list(row) for row in self.rows]})"

    def __getitem__(self, index: tuple[int, int]) -> float:
        i, j = index
        return self.rows[i][j]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Matrix):
            return NotImplemented
        return self.rows == other.rows

    def __hash__(self) -> int:
        return hash(self.rows)

    # Step 3 of the build. Add, returning NotImplemented for other types:
    # m + n, row by row; ValueError("shapes differ: (2, 2) and (2, 3)")
    #   when the shapes differ
    # m @ v with a Vector: a Vector of the dot products row @ v
    # m @ n with a Matrix: a Matrix whose entry (i, j) is row i of m
    #   @ column j of n (the columns of n are the rows of n.T)


if __name__ == "__main__":
    m = Matrix([[1, 2], [3, 4]])
    identity = Matrix([[1, 0], [0, 1]])
    print(m.shape, m[1, 0], m.T)
    print(m + identity)
    print(m @ Vector(1, 1))
    print(m @ identity == m, m @ m)

test_main.py

from main import Matrix, Vector


def test_add():
    """m + n addiert Eintrag für Eintrag"""
    got = Matrix([[1, 2], [3, 4]]) + Matrix([[1, 0], [0, 1]])
    assert got == Matrix([[2, 2], [3, 5]]), f"die Summe ist {got!r}"


def test_add_shape_mismatch():
    """Die Addition von Matrizen verschiedener Form löst ValueError aus"""
    try:
        Matrix([[1, 2]]) + Matrix([[1], [2]])
    except ValueError:
        return
    raise AssertionError("eine (1, 2)- plus eine (2, 1)-Matrix sollte ValueError auslösen")


def test_matrix_times_vector():
    """m @ v ergibt einen Vector"""
    got = Matrix([[1, 2], [3, 4]]) @ Vector(1, 1)
    assert isinstance(got, Vector) and got == Vector(3, 7), f"m @ Vector(1, 1) ist {got!r}, erwartet Vector(3, 7)"


def test_matrix_times_matrix():
    """m @ n ist das Matrixprodukt, auch für nicht quadratische Formen"""
    a = Matrix([[1, 2, 3], [4, 5, 6]])
    b = Matrix([[7, 8], [9, 10], [11, 12]])
    got = a @ b
    assert got == Matrix([[58, 64], [139, 154]]), f"a @ b ist {got!r}"


def test_unsupported_operands():
    """Zahlen werden nicht unterstützt und enden in TypeError"""
    m = Matrix([[1, 2], [3, 4]])
    for text, op in [("m @ 2", lambda: m @ 2), ("m + 1", lambda: m + 1)]:
        try:
            op()
        except TypeError:
            continue
        raise AssertionError(f"{text} sollte TypeError auslösen")

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

self.attr in __getattribute__ lesen

class Logged:
    def __init__(self):
        self.count = 0

    def __getattribute__(self, name):
        self.count += 1
        return object.__getattribute__(self, name)


print(Logged().count)

Was Python ausgibt

RecursionError: maximum recursion depth exceeded

Warum, und die Lösung

self.count in __getattribute__ ist selbst ein Lesezugriff, also ruft sich die Methode endlos selbst auf. Lesen und schreiben Sie eigene Attribute über object.__getattribute__(self, "count") und object.__setattr__, oder nehmen Sie __getattr__, wenn Sie nur die gescheiterten Zugriffe brauchen.

Ein Klassen-Schlüsselwort, das __init_subclass__ nicht annimmt

class Plugin:
    registry = {}

    def __init_subclass__(cls):
        super().__init_subclass__()
        Plugin.registry[cls.__name__] = cls


class Csv(Plugin, name="csv"):
    pass

Was Python ausgibt

TypeError: Plugin.__init_subclass__() got an unexpected keyword argument 'name'

Warum, und die Lösung

Schlüsselwörter in der Klassenzeile gehen an das __init_subclass__ der Elternklasse. Deklarieren Sie die genutzten und reichen Sie den Rest weiter: def __init_subclass__(cls, /, name, **kwargs): super().__init_subclass__(**kwargs).

Zwei fremde Metaklassen mischen

class MetaA(type):
    pass


class MetaB(type):
    pass


class A(metaclass=MetaA):
    pass


class B(metaclass=MetaB):
    pass


class C(A, B):
    pass

Was Python ausgibt

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Warum, und die Lösung

C braucht eine Metaklasse, die Unterklasse von MetaA und MetaB ist, und die gibt es nicht. Definieren Sie class MetaAB(MetaA, MetaB) und nutzen Sie diese, oder, besser, ersetzen Sie die Metaklassen durch __init_subclass__, das sich über normale Vererbung kombiniert.

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

__getattr__ ist der Rückfall, __getattribute__ die Eingangstür

obj.name ruft zuerst __getattribute__ auf, das die ganze Suche erledigt: Daten-Deskriptoren, Instanz-Dictionary, Klasse. Erst wenn dabei AttributeError entsteht, ruft Python __getattr__(name) auf. __getattr__ ist daher günstig und sicher für Standardwerte, Proxys oder berechnete Namen; für Namen, die es nicht behandelt, muss es AttributeError auslösen, sonst versagen hasattr() und getattr() mit Standardwert. Ein eigenes __getattribute__ fängt jeden Zugriff ab, auch self.irgendwas darin: Rufen Sie object.__getattribute__(self, name) auf, um endlose Rekursion zu vermeiden.

__init_subclass__ läuft für jede neue Unterklasse

Erzeugt eine class-Anweisung eine Unterklasse, ruft Python __init_subclass__ auf der Elternklasse auf, mit der neuen Klasse als cls. Die Methode ist implizit eine Klassenmethode und wird für die definierende Klasse selbst nicht aufgerufen. Schlüsselwörter in der Klassenzeile, etwa class Csv(Plugin, name="csv"), werden an sie übergeben; nehmen Sie die benötigten und reichen Sie den Rest an super().__init_subclass__(**kwargs) weiter. Typisch: Registries, Prüfungen, Standardwerte pro Unterklasse.

Metaklassen erzeugen Klassen

Auch Klassen sind Objekte, und ihre Klasse ist ihre Metaklasse, normalerweise type. class Base(metaclass=Meta) lässt Python Meta(name, bases, namespace) aufrufen, um Base zu bauen, und Unterklassen erben die Metaklasse. Meta.__new__ oder __init__ kann so jede Klasse der Hierarchie verändern, Base eingeschlossen. Das ist mächtig, aber schwer: Zwei Basisklassen mit fremden Metaklassen lassen sich nicht kombinieren. Nehmen Sie lieber __init_subclass__, einen Klassen-Dekorator oder Deskriptoren, wenn diese genügen.

Quellen

Zuletzt geprüft am 29. September 2026