07/23: Spielereien mit Metaklassen
Eines der krankeren Features von Python sind sicherlich die Metaklassen. Man liest immer viel davon, letztendlich weiß man sich aber doch nichts darunter vorzustellen und kommt meist auch ohne aus — trotzdem kann es hilfreich sein, zu wissen was sie tun, wo man sie einsetzt — und wo besser nicht.
Ich habe heute eine Situation erlebt in der mir eine Metaklasse sehr gelegen kam, daher möchte ich hier mal ein wenig über meine Erfahrungen berichten.
Was sind Metaklassen?
Metaklassen sind die Dinger, von denen normale Klassen die Objekte sind. Klingt komisch, ist aber so.
Letztlich macht Python mit Klassen so ziemlich das gleiche was jeder Programmierer mit Objekten gewohnt ist: Man erzeugt sie irgendwann, macht etwas damit und löscht sie wieder. Eine Klasse selbst hat dabei außer der Beschreibung, wie ein Objekt aussehen und was es können soll, nicht viel zu tun — Variablen werden im Objekt gespeichert, und die Methoden operieren auch auf dem Objekt. Die Klasse wird erstmal nur gebraucht um das Objekt zu definieren.
Für Metaklassen bedeutet dies: Die Metaklasse legt Methoden fest, die auf der Klasse operieren.
Metaklassen werden also dann interessant, wenn man die Definition einer Klasse zur Laufzeit festlegen oder ändern möchte. Dabei gibt es zwei Bedingungen, die für den Einsatz von Metaklassen sprechen:
- Man möchte eine Klasse zur Laufzeit bearbeiten.
- Man möchte diese Bearbeitung nicht nur an der Klasse selbst vornehmen, sondern auch an allen abgeleiteten Klassen.
Warum diese Bedingungen?
Wenn diese Bedingungen nicht beide erfüllt sind, sehe ich keinen Grund Metaklassen zu benutzen. Soll eine Klasse einmal definiert und dann zur Laufzeit nicht mehr geändert werden (was der Normalfall ist) braucht man sich um Metaklassen keine Gedanken machen, die Klasse type erledigt diesen Fall vollkommen und wird von Python automatisch benutzt, wenn nichts anderes angegeben ist.
Soll nur eine einzige Klasse verändert werden, so gibt es mehrere Möglichkeiten, die etwas einfacher zu verstehen sind:
- Class Factories: Dies sind Methoden, die zur Laufzeit eine Klasse erzeugen und diese zurückgeben. Dabei wird die Klasse in der Regel bei der Erzeugung so aufgebaut, wie sie am Schluss aussehen soll, und danach nicht mehr verändert.
- Dekoratoren: Man kann Dekoratoren auch auf Klassen anwenden, kann also Funktionen schreiben die zur Laufzeit eine Klasse übergeben bekommen und dann mit dieser Klasse etwas anstellen. Dabei kann man die Klasse wie gewohnt programmieren, und überlässt der Funktion die Modifikationen, die zur Laufzeit passieren sollen.
Metaklassen wären in beiden Szenarien Overkill und sollten daher vermieden werden. Metaklassen haben jedoch ein Feature, das sie von diesen beiden Möglichkeiten abhebt: Vererbung.
Wird eine Klasse z.b. mit einem Dekorator modifiziert, so passiert dies bei genau der Klasse, die mit dem Dekorator ausgezeichnet ist, und zwar genau zu dem Zeitpunkt wenn diese Klasse definiert wird — und nie sonst. Es kann allerdings vorkommen dass man alle Klassen modifizieren möchte, die von einer bestimmten Klasse erben. Hier reichen Dekoratoren nicht mehr aus, da sie nicht vererbt werden, sondern explizit aufgerufen werden müssen.
Legt man jedoch eine Metaklasse fest, so wird diese Metaklasse auch für alle Abkömmlinge benutzt, und führt dort die gleichen Modifikationen durch.
Beispiel
Mumble-Django benutzt in seinen Models Properties für diejenigen Felder, die nicht im Model gespeichert werden sollen, sondern die direkt in Murmurs Konfiguration übertragen werden. Djangos ModelForms, die dazu dienen Formulare für Models zu erzeugen und zu verarbeiten, können allerdings nicht mit Properties und ignorieren sie — in meinem Fall ist das ein Problem, denn ich muss die Serverkonfigurationen ja bearbeiten können.
Daher habe ich mir eine Klasse namens PropertyModelForm geschrieben, von der meine Form-Klassen erben. Diese Klasse checkt bei der Instanzierung, welche Felder im Model als Properties ausgeführt sind, und überträgt deren Werte. Damit kann ich Properties bearbeiten.
Ich wollte allerdings nicht nur die Feldinhalte übertragen, sondern habe im Model die Docstrings der Properties mit einem sprechenden Feldnamen versehen und wollte, dass dieser Name im Formular angezeigt wird, ohne dass ich ihn abschreiben muss. Da die PropertyModelForm allein ja keine Felder hat (sondern nur die Übertragung implementiert), wäre es etwas sinnlos gewesen diese Klasse modifizieren zu wollen — interessant sind dabei nur diejenigen Klassen, die von PropertyModelForm erben.
Aus diesem Grund habe ich mich für eine Metaklasse entschieden. Die Klasse PropertyModelFormMeta erledigt in ihrer __init__-Methode die nötigen Modifikationen, indem es die label-Eigenschaft der einzelnen Felder modifiziert wenn die Klasse erzeugt wird. Da Metaklassen weitervererbt werden, geschieht dies vollautomatisch bei allen Formular-Klassen, die von PropertyModelForm abgeleitet werden.
Wie funktioniert das?
Eine Metaklasse ist eine Klasse die vom Typ type erbt. Die interessanteste Methode die man darin überschreiben möchte dürfte __init__ sein, da diese Methode genau dann ausgeführt wird, wenn Python mit dem Lesen des class-Statements der Klasse fertig ist. Damit wird die Klasse direkt modifiziert, sobald sie erzeugt wurde.
Es ist leider etwas schwierig sich dafür Beispiele einfallen zu lassen, daher verweise ich hier auf den Artikel Metaclass programming in Python von IBM. Grundsätzlich läuft es auf folgendes hinaus:
class MeineMetaklasse( type ):
def __init__( cls, name, bases, attrs ):
type.__init__( cls, name, bases, attrs )
# cls ist die erzeugte Klasse — tu was damit!
# z.b.:
if not hasattr( cls, "ping" ):
def ping( self ):
print "pong!"
setattr( cls, "ping", ping )
class MeineKlasseMitPing( object ):
__metaclass__ = MeineMetaklasse
pass
# python ruft an dieser Stelle auf:
# MeineMetaklasse.__init__( MeineKlasseMitPing, "MeineKlasseMitPing", (object,), {...} )
class MeineKlasseOhnePing( object ):
pass
mit = MeineKlasseMitPing()
mit.ping()
ohne = MeineKlasseOhnePing()
ohne.ping()
Wie man sieht besteht der Hauptunterschied zwischen Meta- und normalen Klassen darin, von welcher Klasse sie erben: Die Metaklasse erbt von type, die normalen Klassen von object. Ansonsten definieren beide Arten von Klassen Methoden, die etwas tun.
In diesem Stück Quellcode besteht die Modifikation, die von der Metaklasse an der übergebenen Klasse cls durchgeführt wird, darin eine Methode namens “ping” hinzuzufügen, welche nichts macht als “pong” auszugeben. Ich weiß zwar dass das Beispiel etwas sinnlos ist, es dürfte aber demonstrieren was Metaklassen tun: Sie erhalten eine Klasse übergeben und tun etwas damit, und mit etwas Glück tun sie sogar etwas sinnvolles :)
Zusammenfassung
Metaklassen sind mächtige Hilfsmittel, allerdings muss man genau wissen wo man sie einsetzt. Wenn die beiden genannten Bedingungen nicht erfüllt sind kommt man auch mit anderen Mitteln zum Ziel, die vielleicht einfacher zu verstehen sind. Wenn aber eine Gelegenheit kommt Metaklassen einzusetzen, können sie sehr elegant zum Ziel führen — sie sind also auf jeden Fall einen Blick wert, und wenn man das Prinzip “alles ist ein Objekt” erstmal verstanden hat sind sie auch nicht schwierig zu verstehen.
Comments
Reply to «Spielereien mit Metaklassen»