2 entries in / with tags metaprogramming:

Metaklassen II

Nun, da ich die Theorie hinter Metaklassen etwas beleuchtet habe. Möchte ich ein bisschen zeigen, wofür man Metaklassen verwenden kann und warum und wofür ich sie verwende. Erst einmal ein kleines Beispiel, das verdeutlichen soll, wie man Metaklassen verwendet und wie sie sich "anfühlen":

>>> class CoolType(type):
...   def __new__(cls, name, bases, class_dict):
...     print "Creating new class %r with bases %r and dict %r" % (name, bases, class_dict)
...     # oder: super(CoolType, cls).__new__(cls, name, bases, class_dict)
...     return type.__new__(cls, name, bases, class_dict)
...   def hello(self):
...     print "Hello from %r." % (self,)
...
>>> class CoolClass:
...   __metaclass__ = CoolType
...
Creating new class 'CoolClass' with bases () and dict {'__module__': '__main__', '__metaclass__': <class '__main__.CoolType'>}
>>> type(CoolClass)
<class '__main__.CoolType'>
>>> CoolClass.hello()
Hello from <class '__main__.CoolClass'>.

Wie man sieht, wird das neue Klassenobjekt in __new__ erzeugt, dabei könnte man z.B. das Dictionary der Klasse noch verändern, um der Klasse noch andere Eigenschaften, als die im Klassenblock definierten, zu verleihen. In der Metaklasse definierte Methoden werden direkt zu Klassenmethoden, mit einem wichtigen Unterschied:

>>> c = CoolClass()
>>> c.hello()
  File "<stdin>", line 1, in <module>
AttributeError: 'CoolClass' object has no attribute 'hello'

Instanzen der neu erstellten Klassen haben keine direkte Verbindung zur Klasse ihrer Klasse. Dies ist sinnvoll, da die Metaklasse ja die Eigenschaften der Klasse festlegt, nicht die ihrer Objekte.

Wie im letzten Blog-Eintrag beschrieben, erhalten Unterklassen die Metaklasse ihrer Basisklasse:

>>> class OtherCoolClass(CoolClass):
...   pass
...
Creating new class 'OtherCoolClass' with bases (<class '__main__.CoolClass'>,) and dict {'__module__': '__main__'}
>>> OtherCoolClass.hello()
Hello from <class '__main__.OtherCoolClass'>.

Wichtig ist, dass Python erfordert, dass die Metaklasse einer neuen Klasse Basisklasse der Metaklassen aller Basen sein muss:

class OtherCoolType(type):
...   pass
...
>>> class OtherCoolClass:
...   __metaclass__ = OtherCoolType
...
>>> class TooCoolAClass(CoolClass, OtherCoolClass):
...   pass
...
Creating new class 'TooCoolAClass' with bases (<class '__main__.CoolClass'>, <class '__main__.OtherCoolClass'>) and dict {'__module__': '__main__'}
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __new__
TypeError: Error when calling the metaclass bases
    metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Stellt sich dieses Problem einmal bei extensiver Metaklassenbenutzung, hilft hier der Einsatz einer Metaklassenfaktorfunktion, die eine neue Metaklasse als Unterklasse aller Basismetaklassen erstellt. Wer dazu näheres wissen möchte, der möge sich durch den Quelltext von garbledina wühlen.

In garbledina benutze ich Metaklassen an verschiedenen Stellen. Sie sind vor allem hilfreich, wenn man eine große Anzahl von Klassen definieren möchte, deren gemeinsame Eigenschaften sich nicht allein durch die Basisklasse bestimmen lassen. In SILC gibt es z.B. fast 30 verschiedene Payload-Arten und fast 30 verschiedene Client-Befehle. Eine der Metaklassen die ich hierbei verwende ist garbledina.util.container.ContainerClass. Sie ermöglichst es auf einfache Weise Klassen zu definieren, deren Objekte bestimmte Attribute haben. Die Metaklasse generiert zu einem Tupel von Namen den Konstruktor mit optionalen Default-Argumenten und Accessoren, die dafür sorgen, dass ein Attribut nur einmal gesetzt werden kann:

>>> from garbledina.util import container
>>> class Person(container.Container):
...   properties = ("name", "gender", "birth_date")
...
>>> p = Person("fnord", "fnord", "fnord")
>>> p
<Person: name='fnord', gender='fnord', birth_date='fnord'>

Wie man sieht, macht dies den Quelltext um einiges kürzer und dadurch besser wartbar.


Metaklassen I

Seit Python 2.2 gibt es keinen Unterschied mehr zwischen zwischen Typen und Klassen, wenn diese von object abgeleitet sind. Die, die es nicht sind, werden alte Klassen (classic classes) genannt. Ich persönlich verwende nur noch neue Klassen (new style classes), da diese einige Vorteile gegenüber dem alten Modell bieten. In der Standard-Bibliothek und in vielen anderen Modulen werden aber aus Kompatibilitätsgründen, Bequemlichkeit oder Unwissenheit meist die alten benutzt. (Default, wenn nicht object zu den Basisklassen gehört.) Mit den genauen Unterschieden zwischen alten und neuen Klassen wird sich aber wahrscheinlich ein anderer Blog-Eintrag beschäftigen.

Vor Python 2.2 war type nur eine eingebaute Funktion, die den Typ eines Objekts zurückgegeben hat:

>>> type("")
<type 'str'>
>>> type(1)
<type 'int'>
>>> type(1) is int
True

Da der Unterschied zwischen Funktionen und Klassen in Python nicht so groß ist, funktioniert dies immer noch, type ist nun aber auch eine Klasse, und zwar diejenige, von der alle neuen Klassen sind. Da type selber eine neue Klasse ist, ist type die einzige Klasse, die gleichzeitig ihre eigene Instanz ist:

>>> isinstance(type, type)
True

Wem das nicht verwirrend genug ist, der mag sich über Beziehung von object zu type den Kopf zerbrechen, die hier nicht so wichtig ist:

>>> isinstance(type, object)
True
>>> isinstance(object, type)
True
>>> issubclass(type, object)
True
>>> issubclass(object, type)
False

Es gab mal ein Dokument, das auf diesen Aspekt näher einging und das auch mit hübschen Ascii-Graphiken illustrierte. Leider finde ich den Link dazu nicht mehr.

Um ein neues type-Objekt (eine Klasse) zu erzeugen, kann man type mit drei Argumenten aufrufen: dem Namen der neuen Klasse, dem Tupel der Basisklassen und dem dict-Objekt (Dictionary) der Basisklassen:

>>> type("Test", (), {})
<class '__main__.Test'>

object wird automatisch zu den Basisklassen hinzugefügt, wenn es nicht schon implizit (als Basisklasse einer der Basen) oder explizit als Basis aufgelistet ist. Das Definieren einer Klasse mit einem class-Block ist tatsächlich nur eine syntaktisch schönere Form für so einen Aufruf.

Nun, da klar ist, dass das Erzeugen einer neuen Klasse gleich dem Instanziieren von type (bzw. classobj für alte Klassen) ist, ist zu erwarten, dass man Unterklassen von type erzeugen kann und neue Klassen als Instanzen dieser. Dies ist tatsächlich der Fall. Diese Unterklassen von type heißen Metaklassen (Klassen von Klassen, Klassenklassen). Um herauszufinden von welchem Typ eine neue Klasse sein soll, geht Python den folgenden Weg:

  • Gibt es eine Variable __metaclass__ im Klassenkontext, wird diese genommen.
  • Gibt es Basisklasse, die eine neue Klasse ist, wird deren Klasse genommen.
  • Gibt es nur alte Basisklassen wird classobj (alte Klasse) genommen.
  • Wenn es eine globale Variable __metaclass__ gibt, wird diese genommen.
  • Andernfalls wird classobj genommen.

Der letzte Punkt ist etwas schade, wird sich aber aus Kompatibilitätsgründen erst in Python 3000 ändern. Dadurch sind die meisten Klassen, denen man normalerweise begegnet, alte Klassen (s.o.).