Metaklasse

metaclasses: klein bottle

Eine Metaklasse ist eine Klasse, deren Instanzen Klassen sind. Wie eine "normale" Klasse das Verhalten der Instanzen der Klasse definiert, definiert eine Metaklasse das Verhalten von Klassen und ihren Instanzen.

Metaklassen werden nicht von jeder objektorientierten Programmiersprache unterstützt. Die Programmiersprachen, die Metaklassen unterstützen, unterscheiden sich erheblich in der Art und Weise, wie sie implementiert werden. Python unterstützt Metaklassen.

Einige Programmierer sehen Metaklassen in Python als "Lösungen, die auf ein Problem warten oder suchen".

Es gibt zahlreiche Anwendungsfälle für Metaklassen. Nur um ein paar zu nennen:

  • Protokollierung (logging) und Profilerstellung
  • Konsistenzprüfung von Schnittstellen
  • Registrieren von Klassen zur Erstellungszeit
  • automatisches Hinzufügen neuer Methoden
  • automatische Erstellung von Properties
  • Proxies
  • automatische Ressourcensperre und Synchronisation.

Metaklassen definieren

Grundsätzlich werden Metaklassen wie jede andere Python-Klasse definiert, aber sie sind Klassen, die von type erben. Ein weiterer Unterschied besteht darin, dass eine Metaklasse automatisch aufgerufen wird, wenn die Klassenanweisung mit einer Metaklasse endet. Mit anderen Worten: Wenn nach den Basisklassen (möglicherweise gibt es auch keine Basisklassen) des Klassenkopfs kein Schlüsselwort "metaclass" übergeben wird, wird type() (d. h. __call__ von der type-Klasse) aufgerufen. Wenn andererseits ein Metaklassen-Schlüsselwort verwendet wird, wird die ihm zugewiesene Klasse anstelle des Typs aufgerufen.

Jetzt erstellen wir eine sehr einfache Metaklasse. Sie ist für nichts nützlich, außer dass sie den Inhalt ihrer Argumente in der Methode __new__ ausgibt und die Ergebnisse des Aufrufs type.__new__ zurückliefert:

class LittleMeta(type):
    def __new__(cls, clsname, superclasses, attributedict):
        print("clsname: ", clsname)
        print("superclasses: ", superclasses)
        print("attributedict: ", attributedict)
        return type.__new__(cls, clsname, superclasses, attributedict)

Wir werden die Metaklasse "LittleMeta" im folgenden Beispiel verwenden:

class S:
    pass
class A(S, metaclass=LittleMeta):
    pass
a = A()
clsname:  A
superclasses:  (<class '__main__.S'>,)
attributedict:  {'__module__': '__main__', '__qualname__': 'A'}

Wir können sehen, dass LittleMeta.__ new__ aufgerufen wurde und nicht type.__ new__.

Wiederaufnahme unseres Threads aus dem letzten Kapitel: Wir definieren eine Metaklasse EssentialAnswers, die unsere augment_answer-Methode automatisch einbinden kann:

x = input("Benötigst du die Antwort=? (j/n): ")
if x.lower() == "j":
    benötigt = True
else:
    benötigt = False
    
def der_antwort(self, *args):              
    return 42
    
class EssentialAnswers(type):
    
    def __init__(cls, clsname, superclasses, attributedict):
        if benötigt:
            cls.der_antwort = der_antwort
                           
    
class Philosoph1(metaclass=EssentialAnswers): 
    pass
class Philosoph2(metaclass=EssentialAnswers): 
    pass
class Philosoph3(metaclass=EssentialAnswers): 
    pass
    
    
plato = Philosoph1()
print(plato.der_antwort())
kant = Philosoph2()
# lass uns sehen was Kant zu sagen hat :-)
print(kant.der_antwort())
42
42

In unserem Kapitel "Typ- und Klassenbeziehung" haben wir erfahren, dass Python nach der Verarbeitung der Klassendefinition aufruft

type(classname, superclasses, attributes_dict)

Dies ist nicht der Fall, wenn im Header eine Metaklasse deklariert wurde. Das haben wir in unserem vorherigen Beispiel getan. Unsere Klassen Philosoph1, Philosoph2 und Philosoph3 wurden mit der Metaklasse EssentialAnswers verknüpft. Aus diesem Grund wird EssentialAnswer anstelle von type aufgerufen:

EssentialAnswer(classname, superclasses, attributes_dict)

Um genau zu sein, werden für die Argumente der Aufrufe die folgenden Werte festgelegt:

EssentialAnswer('Philopsopher1', 
                (), 
                {'__module__': '__main__', '__qualname__': 'Philosopher1'})

Die anderen Philosophenklassen werden analog behandelt.

Erstellen von Singletons mit Metaklassen

Das Singleton-Muster ist ein Entwurfsmuster, das die Instanziierung einer Klasse auf ein Objekt beschränkt. Es wird in Fällen verwendet, in denen genau ein Objekt benötigt wird. Das Konzept kann verallgemeinert werden, um die Instanziierung auf eine bestimmte oder feste Anzahl von Objekten zu beschränken. Der Begriff stammt aus der Mathematik, wo ein Singleton - auch als Einheitensatz bezeichnet - für Mengen mit genau einem Element verwendet wird.

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]
    
    
class SingletonClass(metaclass=Singleton):
    pass
class RegularClass():
    pass
x = SingletonClass()
y = SingletonClass()
print(x == y)
x = RegularClass()
y = RegularClass()
print(x == y)
True
False

Erstellen von Singletons mit Metaklassen

Alternativ können wir Singleton-Klassen erstellen, indem wir von einer Singleton-Klasse erben, die wie folgt definiert werden kann:

class Singleton(object):
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = object.__new__(cls, *args, **kwargs)
        return cls._instance
    
class SingletonClass(Singleton):
    pass
class RegularClass():
    pass
x = SingletonClass()
y = SingletonClass()
print(x == y)
x = RegularClass()
y = RegularClass()
print(x == y)
True
False