Auf dem Weg zu Metaklassen

Motivation

Weg zur Metaklasse In diesem Kapitel unseres Tutorials möchten wir einige Ideen und Anreize zur Nutzung von Metaklassen präsentieren. Um ein paar Design-Probleme zu demonstrieren, die mit Metaklassen gelöst werden können, erstellen wir diverse "Philosopher"-Klassen. Jede "Philosopher"-Klasse (Philosopher1, Philosopher2, und so weiter) benötigt die gleiche "Menge" an Methoden. In unserem Beispiel nur eine - "the_answer" - als Basis ihres Grübelns und Nachdenkens. Ein schlechte Implementierung erreichen wir, indem wir in jede Philospoher-Klasse den identischen Code schreiben:
class Philosopher1: 
    def the_answer(self, *args):              
        return 42
        
class Philosopher2: 
    def the_answer(self, *args):              
        return 42
        
class Philosopher3: 
    def the_answer(self, *args):              
        return 42
        
plato = Philosopher1()
print(plato.the_answer())

kant = Philosopher2()
# let's see what Kant has to say :-)
print(kant.the_answer())
Folgende Ausgabe ist nicht schwer nachzuvollziehen:
42
42
Wir stellen fest, dass wir mehrere Kopien der Methode "the_answer" implementiert haben. Das ist ziemlich Fehleranfällig und bedeutet mehr Aufwand bei Wartungen.
Bisher wissen wir, dass der einfachste Weg redundanten Code zu vermeiden darin besteht, eine Basis-Klasse zu schaffen, welche die Methode "the_answer" enthält. Jede Philosopher-Klasse erbt dann von der Basis-Klasse:
class Answers:
    def the_answer(self, *args):              
        return 42
    
class Philosopher1(Answers): 
    pass
    
class Philosopher2(Answers): 
    pass
    
class Philosopher3(Answers): 
    pass
    
plato = Philosopher1()
print(plato.the_answer())

kant = Philosopher2()
# let's see what Kant has to say :-)
print(kant.the_answer())
Auch hier wieder die gleiche Ausgabe wie im oberen Beispiel:
42
42
Auf diese Art hat jede Philosopher-Klasse immer die Methode "the_answer". Nehmen wir an, dass wir noch nicht wissen, ob die Methode gebraucht wird. Nehmen wir weiter an, dass die Entscheidung darüber, ob die Methode gebraucht wird, zur Laufzeit getroffen wird. Diese Entscheidung kann abhängig sein von Konfigurations-Dateien, Benutzereingaben oder Berechnungen.
# the following variable would be set as the result of a runtime calculation:
x = input("Do you need the answer? (y/n): ")
if x:
    required = True
else:
    required = False
    
def the_answer(self, *args):              
        return 42
        
class Philosopher1: 
    pass
    
if required:
    Philosopher1.the_answer = the_answer

class Philosopher2: 
    pass

if required:
    Philosopher2.the_answer = the_answer

class Philosopher3: 
    pass

if required:
    Philosopher3.the_answer = the_answer
    
    
plato = Philosopher1()
kant = Philosopher2()
# let's see what Plato and Kant have to say :-)
if required:
    print(kant.the_answer())
    print(plato.the_answer())
else:
    print("The silence of the philosphers")
Das Programm liefert folgende Ausgabe:
Do you need the answer? (y/n): y
42
42
Falls wir die Frage nicht mit ,,y'' beantworten, sieht das Ergebnis so aus:
Do you need the answer? (y/n): y
42
42

Auch bei dieser Lösung gibt es noch Nachteile. Sie ist ebenfalls fehleranfällig, weil wir wieder den gleichen Code zu jeder Klasse schreiben müssen. Wenn wir viele Methoden hinzufügen wollen, kann das ziemlich unübersichtlich werden.
Wir können unseren Ansatz verbessern, indem wir eine Manager-Funktion definieren, um redundanten Code weiter zu vermeiden. Die Manager-Funktion übernimmt die Aufgabe, die Klassen entsprechend zu erweitern.
# the following variable would be set as the result of a runtime calculation:
x = input("Do you need the answer? (y/n): ")
if x:
    required = True
else:
    required = False
    
def the_answer(self, *args):              
        return 42
        
# manager function
def augment_answer(cls):                      
    if required:
        cls.the_answer = the_answer
        
class Philosopher1: 
    pass
    
augment_answer(Philosopher1)

class Philosopher2: 
    pass

augment_answer(Philosopher2)

class Philosopher3: 
    pass

augment_answer(Philosopher3)
    
    
plato = Philosopher1()
kant = Philosopher2()
# let's see what Plato and Kant have to say :-)
if required:
    print(kant.the_answer())
    print(plato.the_answer())
else:
    print("The silence of the philosphers")
Dies ist eine brauchbare Lösung für unser Problem, jedoch müssen wir darauf achten, dass wir den Aufruf der Manager-Funktion "augment_answer" nicht vergessen. Der Code sollte automatisch aufgerufen werden. Wir brauchen eine Möglichkeit um sicherzustellen, dass "bestimmter" Code automatisch im Anschluss einer Klassen-Definition ausgeführt wird.

Im folgenden benutzen wir einen Klassendekorator, d.h. wir schreiben die "augment_answer"-Funktion als Dekoratorfunktion um. Die Philosopher-Klassen können wir nun entsprechend dekorieren.
# the following variable would be set as the result of a runtime calculation:
x = input("Do you need the answer? (y/n): ")
if x:
    required = True
else:
    required = False
    
def the_answer(self, *args):              
        return 42
        
def augment_answer(cls):                      
    if required:
        cls.the_answer = the_answer
    # we have to return the class now:
    return cls
        
@augment_answer
class Philosopher1: 
    pass
    
@augment_answer
class Philosopher2: 
    pass
    
@augment_answer
class Philosopher3: 
    pass
    
    
plato = Philosopher1()
kant = Philosopher2()
# let's see what Plato and Kant have to say :-)
if required:
    print(kant.the_answer())
    print(plato.the_answer())
else:
    print("The silence of the philosphers")
Das Programm liefert folgende Ausgabe:
Do you need the answer? (y/n): y
42
42
Im kommenden Kapitel lernen wir, dass Metaklassen für diesen Zweck sehr nützlich sein können.