0%

Dataclasses und Wertobjekte · Übung

Klassenattribute und Instanzattribute

Jedes Attribut, das du bisher geschrieben hast, wurde in __init__ zugewiesen oder in einer Dataclass aufgeführt. Jedes Objekt erhielt sein eigenes. Es gibt einen zweiten Ort für ein Attribut: Es kann zur Klasse gehören statt zu einem einzelnen Objekt.

Ein Wert für die ganze Klasse

Try it

default_points wird im Klassenkörper zugewiesen, nicht in __init__. Es gibt genau einen solchen Wert, den jedes Question teilt. Er lässt sich über die Klasse selbst lesen, ohne dass ein Objekt existiert.

Instanzattribute beantworten „Was gilt für dieses eine Objekt?“ Klassenattribute beantworten „Was gilt für jedes Objekt dieser Art?“

Lesen und die Überraschung

Lesen funktioniert über beide Wege. Zuweisen tut nicht das, wonach es aussieht:

Try it

5, 1, 1.

Die Zuweisung über ein Objekt hat das Klassenattribut nicht geändert. Sie hat auf first ein neues Instanzattribut erzeugt, das es verdeckt. second und die Klasse bleiben unangetastet. first hat jetzt einen eigenen Wert, der nicht mehr dem Klassenwert folgt.

Um den Klassenwert für alle zu ändern, weise über die Klasse zu:

Try it

first meldet weiterhin 5, weil es jetzt einen eigenen Wert hat. second meldet 3, weil es noch die Klasse liest.

Ein Klassenattribut wird von Instanzen geteilt, bis eine Instanz es mit einem eigenen Wert verdeckt.

Warum meldet second.default_points nach first.default_points = 5 noch den alten Wert?

Der Fall, der Probleme macht

Diese Regel ist meist nur eine Besonderheit. Zum Fehler wird sie, wenn das Klassenattribut veränderbar ist:

Try it

Die Abschlussprüfung enthält die Frage aus der Wiederholung. self.questions.append(...) hat nie etwas zugewiesen, daher entstand kein Instanzattribut. Es fand die eine gemeinsame Liste und hängte die Frage an.

Das ist dieselbe Falle wie beim veränderbaren Standardwert aus Lektion 4, nur über einen anderen Zugang. Auch die Lösung folgt derselben Idee: Gib jedem Objekt seinen eigenen Wert.

class Quiz:
    def __init__(self, title):
        self.title = title
        self.questions = []

Verwende in einer Dataclass field(default_factory=list). Ein veränderbares Klassenattribut ist gemeinsamer Zustand. Verwende es nur, wenn dieses Teilen beabsichtigt ist und die Klasse klare Vorgänge dafür anbietet. Sammlungen pro Objekt gehören zu jeder einzelnen Instanz.

Wofür Klassenattribute wirklich gut sind

Für ihren eigentlichen Zweck sind sie nützlich und sicher: Konstanten, die zur Klasse gehören.

Try it

MAX_POINTS ist eine Aussage über Fragen im Allgemeinen. Es in der Klasse zu halten ist daher passender als eine Variable auf Modulebene. Von überall ist es als Question.MAX_POINTS lesbar. Der großgeschriebene Name signalisiert wie üblich, dass der Wert nicht geändert werden soll.

Beachte, was @dataclass mit diesen beiden Zeilen getan hat: nichts. Sie haben keine Annotation, sind deshalb keine Felder, erscheinen weder in __init__ noch in der repr-Darstellung und werden nicht verglichen. Bei der Syntax dieses Kapitels ist ein annotierter Name ein Feld und ein nicht annotierter Name ein gewöhnliches Klassenattribut. Fortgeschrittene Annotationen wie typing.ClassVar können ein annotiertes Klassenattribut ausdrücklich kennzeichnen. Das liegt außerhalb dieser Lektion.

Try it

Kein MAX_POINTS in der repr-Darstellung, genau wie beabsichtigt.

Die Übung verwendet Klassenattribute für zwei passende Aufgaben und repariert eines, das für die falsche Aufgabe eingesetzt wurde.

Aufgabe

Verwende Klassenattribute für passende Aufgaben und nicht für ungeeignete.

Question soll MIN_POINTS = 1 und MAX_POINTS = 10 als Klassenattribute ohne Annotation enthalten, damit sie außerhalb des erzeugten __init__, der repr-Darstellung und der Gleichheit bleiben. Verwende sie in __post_init__, um Punkte außerhalb des einschließlich der Grenzen gültigen Bereichs abzulehnen. Fragetext und erwartete Antwort dürfen weiterhin nicht leer sein.

Quiz hat einen Fehler: _questions ist ein veränderbares Klassenattribut, daher teilen alle Quizze eine Liste. Gib jedem Quiz seine eigene interne Liste _questions, lehne None in add ab und gib aus questions() eine Kopie zurück.

QuizAttempt soll mit einem Klassenattribut count zählen, wie viele Versuche erzeugt wurden. __init__ erhöht es über die Klasse. Jeder Versuch soll dieselbe Gesamtzahl melden, denn sie beschreibt Versuche im Allgemeinen statt einen einzelnen.

Führe das Programm aus. Die Abschlussprüfung darf keine Fragen aus der Wiederholung enthalten, eine Frage mit unzulässiger Punktzahl muss abgelehnt werden, und beide Versuche müssen dieselbe Anzahl melden.