Auf dem Weg zu Metaklassen

Motivation

Weg zur Metaklasse In diesem Kapitel möchten wir Sie für die Nutzung von Metaklassen motivieren. Um ein paar Design-Probleme zu demonstrieren, die mit Metaklassen gelöst werden können, erstellen wir Philospher-Klassen. Jede Philosopher-Klasse benötigt die gleiche "Menge" an Methoden. In unserem Beispiel nur eine - "the_answer" - als Basis der Nachdenklichkeit. 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 zu schaffen, welche 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, 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
Selbst bei dieser Lösung gibt es noch Nachteile. Es ist Fehleranfällig weil wir wieder den selben 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")
Auch das 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 ausgeführt wird im Anschluss einer Klassen-Definition.
# 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.