Auf dem Weg zu Metaklassen

Motivation für Metaklassen

on the road to metaclasses

In diesem Kapitel unseres Tutorials möchten wir einige Anreize oder Motivationen für die Verwendung von Metaklassen bieten. Um einige Designprobleme zu demonstrieren, die durch Metaklassen gelöst werden können, werden wir eine Reihe von Philosophenklassen vorstellen und entwerfen. Jede Philosophenklasse (Philosoph1, Philosoph2 usw.) benötigt dieselbe "Menge" von Methoden (in unserem Beispiel nur eine, d. h. die_antwort) als Grundlage für das Nachdenken und Grübeln. Man sollte sich natürlich vorstellen, dass die KLassen noch unterschiedliche Methoden haben, die wir aber nicht implementiert haben. Eine dumme Art, die Klassen zu implementieren, wäre, in jeder Philosphenklasse den gleichen Code für die_antwort zu haben:

class Philosoph1(object): 
    
    def die_antwort(self, *args):              
        return 42
class Philosoph2(object): 
    def die_antwort(self, *args):              
        return 42
class Philosoph3(object): 
    def die_antwort(self, *args):              
        return 42
plato = Philosoph1()
print (plato.die_antwort())
kant = Philosoph2()
# lass uns sehen was Kant zu sagen hat :-)
print (kant.die_antwort())
42
42

Wir können sehen, dass wir mehrere Kopien der Methode "die_antwort" haben. Dies ist natürlich fehleranfällig und mühsam zu warten.

Nach dem, was wir bisher wissen, besteht der einfachste Weg, unser Ziel zu erreichen, ohne redundanten Code zu erstellen, darin, eine Basis zu entwerfen, die "die_antwort" als Methode enthält. Jetzt erbt jede Philosophenklasse von dieser Basisklasse:

class Antworten(object):
    def die_antwort(self, *args):              
        return 42
    
class Philosoph1(Antworten): 
    pass
class Philosoph2(Antworten): 
    pass
class Philosoph3(Antworten): 
    pass
plato = Philosoph1()
print(plato.die_antwort())
kant = Philosoph2()
# lass uns sehen was Kant zu sagen hat :-)
print(kant.die_antwort())
42
42

So wie wir unsere Klassen entworfen haben, wird jede Philosophen-Klasse immer eine Methode "die_antwort" haben. Nehmen wir an, wir wissen nicht von vornherein, ob wir diese Methode wollen oder brauchen. Nehmen wir an, dass die Entscheidung, ob die Klassen erweitert werden müssen, nur zur Laufzeit getroffen werden kann. Diese Entscheidung kann von Konfigurationsdateien, Benutzereingaben oder einigen Berechnungen abhängen.

#Die folgende Variable wird als Ergebnis einer Laufzeitberechnung festgelegt:
x = input("Benötigst du die Antwort?? (j/n): ")
if x == "j":
    benötigt = True
else:
    benötigt = False
    
def die_antwort(self, *args):              
        return 42
class Philosoph1(object): 
    pass
if benötigt:
    Philosoph1.die_antwort = die_antwort
class Philosoph2(object): 
    pass
if benötigt:
    Philosoph2.die_antwort = die_antwort
class Philosoph3(object): 
    pass
if benötigt:
    Philosoph3.die_antwort = die_antwort
    
    
plato = Philosoph1()
kant = Philosoph2()
# lass uns sehen was Kant und Plato zu sagen haben :-)
if benötigt:
    print(kant.die_antwort())
    print(plato.die_antwort()) 
else:
    print("Das Schweigen der Philosophen")
42
42

Obwohl dies eine weitere Lösung für unser Problem ist, gibt es dennoch einige schwerwiegende Nachteile. Es ist fehleranfällig, da wir jeder Klasse den gleichen Code hinzufügen müssen, und es sehr gut möglich ist, dass wir es vergessen. Außerdem wird es kaum beherrschbar und vielleicht sogar verwirrend, wenn es viele Methoden gibt, die wir hinzufügen möchten.

Wir können unseren Ansatz verbessern, indem wir eine Managerfunktion definieren und auf diese Weise redundanten Code vermeiden. Die Manager-Funktion wird verwendet, um die Klassen bedingt zu erweitern.

x = input("Benötigst du die Antwort?? (j/n):'? ")
if x == "j":
    benötigt = True
else:
    benötigt = False
    
def die_antwort(self, *args):              
        return 42
# Managerfunktion
def augment_antwort(cls):                      
    if benötigt:
        cls.die_antwort = die_antwort
        
    
class Philosoph1(object): 
    pass
augment_antwort(Philosoph1)
class Philosoph2(object): 
    pass
augment_antwort(Philosoph2)
class Philosoph3(object): 
    pass
augment_antwort(Philosoph3)
    
    
plato = Philosoph1()
kant = Philosoph2()
# lass uns sehen was Kant und Plato zu sagen haben :-)
if benötigt:
    print(kant.die_antwort())
    print(plato.die_antwort())
else:
    print("Das Schweigen der Philosophen")
42
42

Auch wenn dies besser ist als die vorige Lösung, müssen wir, d. h. die Klassendesigner, darauf achten, dass wir unmittelbar nach der Klassendefinition die Managerfunktion aufrufen. Besser wäre es, wenn der Code automatisch ausgeführt würde. Wir brauchen einen Weg um sicherzustellen, dass bestimmter Code nach dem Ende einer Klassendefinition automatisch ausgeführt werden.

x = input("Benötigst du die Antwort?? (j/n):'? ")
if x== "j":
    benötigt = True
else:
    benötigt = False
def die_antwort(self, *args):              
        return 42
def augment_antwort(cls):                      
    if benötigt:
        cls.die_antwort = die_antwort
    # wir müssen jetzt die Klasse zurückgeben:
    return cls
        
@augment_antwort
class Philosoph1(object): 
    pass
@augment_antwort
class Philosoph2(object): 
    pass
@augment_antwort
class Philosoph3(object): 
    pass
    
plato = Philosoph1()
kant = Philosoph2()
    
#lass uns sehen was Kant und Plato zu sagen haben :-)
if benötigt:
    print(kant.die_antwort())
    print(plato.die_antwort())
else:
    print("Das Schweigen der Philosophen")
42
42

Zu diesem Zweck können auch Metaklassen verwendet werden, wie wir im nächsten Kapitel erfahren werden.