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.