PCPP1 Block 1 (Teil 3b): pickle, shelve und Metaklassen

Track Python · PCPP1 Block 1 (35 %, 15 Fragen laut Quelle) · Ziele 1.11 und 1.12 · ca. 45 Min.

Was du aus Teil a brauchst

Aus Teil 3a brauchst du den Unterschied zwischen flacher und tiefer Kopie: Beim Speichern und Laden mit pickle und shelve entstehen ebenfalls neue Objekte, und genau das erklärt die Shelf-Falle. Für Metaklassen reicht, dass eine Klasse in Python selbst ein Objekt ist.

Worum es geht

Das ist die zweite Hälfte von PCPP1 Block 1 Teil 3 (Fortgeschrittene OOP, laut Quelle 35 % und 15 Fragen). Du kennst JSON.stringify und localStorage aus JS/TS. In Python gibt es dafür pickle und shelve mit einer Sicherheitsfalle und einer Verlustfalle, dazu Metaklassen (metaclasses), also Klassen, die Klassen herstellen. Nach der Lektion kannst du den shelve-Verlust bei veränderbaren Werten vermeiden, das Verhalten einer Metaklasse vorhersagen und eine Metaklasse schreiben, die Klassen registriert.

Von JS/TS her gedacht

Thema JS/TS Python
Objekt als Text/Bytes speichern JSON.stringify (nur Daten) json (Text, wenige Typen) oder pickle (binär, fast alles)
Persistente Key-Value-Datei localStorage (Browser) shelve: Dict-ähnlich, auf Basis von pickle
Klasse zur Laufzeit bauen class ist Syntax, Prototypen von Hand type(name, bases, dict) baut eine Klasse
Klassen “überwachen” Decorators (Stage 3) oder Proxy Metaklasse (class A(metaclass=M))

Konzept in kleinen Schritten

Schritt 1: Serialisierung mit pickle und shelve

Serialisierung (serialization) wandelt ein Objekt in einen Byte-Strom um, Deserialisierung (deserialization) stellt es wieder her. Zweck laut Quelle: Persistenz und Datenaustausch (Abschnitt 1.11).

  • pickle.dumps(obj) liefert bytes, pickle.loads(b) stellt das Objekt wieder her. dump(obj, file) und load(file) arbeiten mit Dateien im Binärmodus ("wb", "rb").
  • Nicht alles ist pickle-fähig, z. B. Lambdas und offene Dateien nicht.
  • JSON gegen pickle: JSON ist Text und sprachunabhängig, kennt aber nur wenige Typen. Pickle ist binär, Python-spezifisch und kann fast jedes Objekt speichern.

Sicherheit (security, Quelle): Lade nie Pickle-Daten aus nicht vertrauenswürdigen Quellen, beim Laden kann beliebiger Code ausgeführt werden. Das zeigt dieses harmlose Beispiel: Beim Laden läuft eine Funktion, die der Autor der Daten bestimmt hat.

Statt print könnte dort jede andere Funktion stehen. Für Daten aus fremden Quellen nimmst du JSON.

shelve ist ein persistentes, Dict-ähnliches Objekt auf Basis von pickle. Schlüssel müssen Strings sein. Der Modus (flag) von shelve.open(dateiname, flag=...):

flag Bedeutung (Quelle)
"r" nur lesen
"w" lesen und schreiben, Datei muss existieren
"c" lesen und schreiben, anlegen falls nötig (Standard)
"n" immer neu und leer anlegen

Ein typischer Aufruf (hier nur als Text, nicht ausführbar, weil er eine Datei anlegt):

import shelve
with shelve.open("store") as db:
    db["user"] = {"name": "Ada"}
with shelve.open("store", flag="r") as db:
    print(db["user"])                # {'name': 'Ada'}

Die Falle: Änderst du ein aus dem Shelf gelesenes veränderbares Objekt, geht die Änderung verloren. Ausnahmen: Du weist es neu zu, oder du öffnest mit writeback=True. Damit die Zelle im Browser läuft (kein Dateizugriff nötig), nutzen wir shelve.Shelf über einem normalen Dict als Speicher. Das Verhalten ist dasselbe wie bei shelve.open.

Warum [1]? Jedes db["liste"] entpickelt (unpickle) den Wert frisch. Das append landet auf einem Objekt, das niemand mehr festhält. Ohne writeback schreibt nur db[key] = wert.

Schritt 2: Metaprogrammierung und Metaklassen

Laut Quelle (Abschnitt 1.12):

  • Metaklasse: eine Klasse, deren Instanzen Klassen sind. Die Standard-Metaklasse ist type. Es gilt type(type) is type.
  • type(name, bases, dict) erzeugt eine Klasse dynamisch.
  • Eigene Metaklasse: class Meta(type) mit __new__ oder __init__, verwendet über class A(metaclass=Meta).
  • Metaklassen werden selten gebraucht. Einfachere Alternativen sind Klassendekoratoren und __init_subclass__.
  • Spezialattribute: __name__ (Klassenname), __class__ (Klasse eines Objekts, bei einer Klasse ihre Metaklasse), __bases__ (Tupel der Basisklassen), __dict__ (Namensraum).

__new__ der Metaklasse erzeugt die Klasse (hier kannst du den Namensraum ns noch ändern, und du musst die neue Klasse zurückgeben). __init__ der Metaklasse läuft danach mit der fertigen Klasse als erstem Argument (cls), geeignet zum Registrieren. Die Signatur ist __init__(cls, name, bases, ns), und wie bei jeder Überschreibung gehört super().__init__(name, bases, ns) hinein (Übung 3). Ein gemeinsames Wörterbuch legst du als Attribut an der Metaklasse selbst ab, dann sehen es alle von ihr erzeugten Klassen.

Die einfacheren Alternativen im Vergleich:

Faustregel: Reicht __init_subclass__ oder ein Dekorator, nimm diese. Eine Metaklasse brauchst du, wenn die Klasse selbst (ihr Typ, ihr Namensraum beim Erzeugen) beeinflusst werden soll. Die Quelle nennt __init_subclass__ und Klassendekoratoren nur als einfachere Alternativen. Die genauen Grenzen, wann die Metaklasse wirklich nötig ist, stehen nicht in der Quelle (bitte prüfen).

Falle: die Prüfungs- und Praxisklassiker

  1. Pickle nie auf fremde Daten anwenden. Beim Laden kann beliebiger Code laufen. Lambdas und offene Dateien lassen sich nicht pickeln.
  2. Shelf-Änderungen an veränderbaren Werten gehen ohne writeback verloren. Neu zuweisen oder writeback=True.
  3. A.__class__ ist bei einer Klasse mit Metaklasse die Metaklasse, nicht type.

Weitere Fallen von Block 1 (__eq__ ohne __hash__, __getattr__ gegen __getattribute__, Dekorator ohne wraps, gestapelte Dekoratoren, ABC ohne @abstractmethod, überschriebene Methoden in list und dict, inkonsistente MRO) stehen in den Lektionen 10 bis 12b und der Liste der Quelle.

Übungen

Übung 1: Shelf-Verlust vermeiden

db ist ein shelve.Shelf ohne writeback. Schreibe anhaengen(db, key, wert), das wert an die Liste unter key anhängt und die Änderung wirklich speichert. Gibt es den Schlüssel noch nicht, wird die Liste mit [wert] angelegt. Andere Schlüssel bleiben unberührt. writeback=True zu setzen ist keine Lösung, du kannst es nicht beeinflussen.

Schau dir in Schritt 1 noch einmal an, warum db["liste"].append(2) ins Leere läuft, und welche der beiden Ausnahmen aus der Quelle ohne Zugriff auf writeback übrig bleibt. Denke auch an den Fall, dass der Schlüssel fehlt: db[key] wirft dann etwas.

import shelve

def anhaengen(db, key, wert):
    liste = db[key] if key in db else []
    liste.append(wert)
    db[key] = liste

anhaengen

Übung 2: Metaklasse vorhersagen

log = []

class Meta(type):
    def __new__(mcs, name, bases, ns):
        log.append("new " + name)
        ns["tag"] = name.upper()
        return super().__new__(mcs, name, bases, ns)

    def __init__(cls, name, bases, ns):
        log.append("init " + name)
        super().__init__(name, bases, ns)

class A(metaclass=Meta):
    pass

class B(A):
    pass

Trage das Tupel (log, B.tag, type(B).__name__, B.__class__ is type, B().__class__.__name__, B.__bases__[0].__name__) ein.

Eine Metaklasse läuft bei jeder Klassendefinition, auch bei Unterklassen, weil die Metaklasse vererbt wird. Denke an die Reihenfolge von __new__ und __init__ pro Klasse, und daran, welchen Namensraum ns jede Klasse für sich bekommt. Was ist die Klasse einer Klasse?

antwort = (["new A", "init A", "new B", "init B"], "B", "Meta", False, "B", "A")
antwort

Übung 3: Eine Registry-Metaklasse schreiben

Schreibe die Metaklasse Registry, die jede Klasse, die sie erzeugt (auch die Basisklasse Plugin selbst und alle Unterklassen, auch Unter-Unterklassen), im Klassenattribut Registry.registry ablegt. Der Schlüssel ist der kleingeschriebene Klassenname, der Wert die Klasse. Normale Klassen ohne diese Metaklasse dürfen nicht eingetragen werden. Plugin ist die Basisklasse, die Registry als Metaklasse verwendet. Nutze die Metaklasse, nicht __init_subclass__.

Beim __init__ einer Metaklasse ist cls schon die fertige Klasse. Überlege, wo das Wörterbuch liegt (an der Metaklasse oder an der einzelnen Klasse?) und ob du den Eintrag nur für Unterklassen oder auch für die Basis brauchst. Wie sagst du Python bei der Klassendefinition, welche Metaklasse gelten soll?

class Registry(type):
    registry = {}

    def __init__(cls, name, bases, ns):
        super().__init__(name, bases, ns)
        Registry.registry[name.lower()] = cls

class Plugin(metaclass=Registry):
    pass

(Registry, Plugin)

Merksatz und Prüfstein

Merksatz: pickle nie auf fremde Daten, ein Shelf speichert nur Zuweisungen, und eine Metaklasse ist die Klasse einer Klasse.

Prüfstein (offene Frage): Jede Unterklasse von Plugin soll sich automatisch registrieren. Wann reicht dafür __init_subclass__, und wann brauchst du eine Metaklasse? Und warum ist es riskant, eine Pickle-Datei zu laden, die ein Kunde dir schickt, und was nimmst du stattdessen?

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitte “1.11 Serialisierung mit pickle und shelve”, “1.12 Metaprogrammierung und Metaklassen” und “Typische Fallen in Block 1”. Hinweis: Das Verhalten von shelve.Shelf über einem Dict, __reduce__ als Beispiel für Codeausführung beim Laden, __init_subclass__ als Aufrufzeitpunkt und die Reihenfolge von __new__ und __init__ der Metaklasse bei Unterklassen stehen nicht (oder nur teilweise) in der Quelle, wurden aber mit Python 3.13 ausgeführt und geprüft (bitte prüfen, ob sie in der PCPP1 geprüft werden). Das Verhalten von shelve.open mit Dateien unter Pyodide wurde nicht getestet (deshalb shelve.Shelf über einem Dict). Die Gewichtung “35 %, 15 Fragen” ist die Angabe der Quelle (bitte prüfen).