Methodenaufrufe zählen mit einer Metaklasse

Einführung

Zählen von Funktionsaufrufen Nach der Durcharbeit des Kapitels "Metaklassen" fragen Sie sich vielleicht, was mögliche Anwendungsfälle für Metaklassen sein könnten. Es gibt ein paar interessante Anwendungsfälle und es ist nicht "eine Lösung, die auf ein Problem wartet". Ein paar Beispiele haben wir bereits genannt.

In diesem Kapitel möchten wir ein Metaklassen-Beispiel ausarbeiten, dass die Methoden der Subklasse dekoriert. Die dekorierte Funktion, die vom Dekorateur zurückgeliefert wird, macht es möglich die Anzahl der Aufrufe der Subklasse zu zählen.

Das ist normalerweise eine der Aufgaben, die wir von einem Profiler erwarten. Wir können die Metaklasse benutzen für einfaches Profiling. Die Metaklasse ist einfach zu erweitern für weitere Profiling-Aufgabe.

Vorbereitungen

Bevor wir uns dem Problem annehmen, möchten wir noch daran erinnern wie auf Attribute iner Klasse zugegriffen wird. Wir demonstrieren dies nochmal anhand einer list-Klasse. Wir erhalten die Liste aller nicht privaten Attribute einer Klasse - hier die random-Klasse - mit folgendem Code:
import random
cls = "random" # name of the class as a string
all_attributes = [x for x in dir(eval(cls)) if not x.startswith("__") ]

print(all_attributes)
Wir erhalten folgende Ausgabe:
['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'Random', 'SG_MAGICCONST', 'SystemRandom', 'TWOPI', '_BuiltinMethodType', '_MethodType', '_Sequence', '_Set', '_acos', '_ceil', '_cos', '_e', '_exp', '_inst', '_log', '_pi', '_random', '_sha512', '_sin', '_sqrt', '_test', '_test_generator', '_urandom', '_warn', 'betavariate', 'choice', 'expovariate', 'gammavariate', 'gauss', 'getrandbits', 'getstate', 'lognormvariate', 'normalvariate', 'paretovariate', 'randint', 'random', 'randrange', 'sample', 'seed', 'setstate', 'shuffle', 'triangular', 'uniform', 'vonmisesvariate', 'weibullvariate']
Anschliessend filtern wir noch die aufrufbaren Attribute heraus bzw. die public-Methoden der Klasse.
methods = [x for x in dir(eval(cls)) if not x.startswith("__") 
                              and callable(eval(cls + "." + x))]
print(methods)
Nun erhalten wir folgende Ausgabe:
['Random', 'SystemRandom', '_BuiltinMethodType', '_MethodType', '_Sequence', '_Set', '_acos', '_ceil', '_cos', '_exp', '_log', '_sha512', '_sin', '_sqrt', '_test', '_test_generator', '_urandom', '_warn', 'betavariate', 'choice', 'expovariate', 'gammavariate', 'gauss', 'getrandbits', 'getstate', 'lognormvariate', 'normalvariate', 'paretovariate', 'randint', 'random', 'randrange', 'sample', 'seed', 'setstate', 'shuffle', 'triangular', 'uniform', 'vonmisesvariate', 'weibullvariate']
Um die nicht aufrufbaren Attribute zu filtern, können wir das Schlüsselwort "not" bei callable einfügen.
non_callable_attributes = [x for x in dir(eval(cls)) if not x.startswith("__") 
                              and not callable(eval(cls + "." + x))]
print(non_callable_attributes)
Ausgabe:
['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'SG_MAGICCONST', 'TWOPI', '_e', '_inst', '_pi', '_random']
Normalerweise ist es weder empfohlen noch notwendig Methoden wir folgt zu verwenden. Aber es ist möglich:
lst = [3,4]
list.__dict__["append"](lst, 42)
lst

[3, 4, 42]

Ein Dekorateur um Funktions-Aufrufe zu zählen

Nun wollen wir beginnen die Metaklasse zu erstellen, die wir bereits am Anfang dieses Kapitels angesprochen haben. Sie wird alle Methoden der Subklasse dekorieren mit dem Dekorateur, der die Aufrufe zählt. Wir haben diesen Dekorateur bereits im Kapitel "Memoisation und Dekorateure" definiert:
def call_counter(func):
    def helper(*args, **kwargs):
        helper.calls += 1
        return func(*args, **kwargs)
    helper.calls = 0
    helper.__name__= func.__name__
    return helper
Wir können ihn wie gewohnt verwenden:
@call_counter
def f():
    pass
print(f.calls)
for _ in range(10):
    f()
    
print(f.calls)
Ausgabe:
0
10
Noch einmal die alternative Schreibweise. Wir benötigen diese für die fertige Metaklasse:
def f():
    pass
f = call_counter(f)
print(f.calls)
for _ in range(10):
    f()
    
print(f.calls)
Ausgabe:
0
10

Die Metaklasse "Aufrufzähler"

Jetzt haben wir alle "Zutaten" beisammen um unsere Metaklasse zu schreiben. Wir fügen unseren call_counter-Dekorateur als static-Methode ein:
class FuncCallCounter(type):
    """ A Metaclass which decorates all the methods of the 
        subclass using call_counter as the decorator
    """
    
    @staticmethod
    def call_counter(func):
        """ Decorator for counting the number of function 
            or method calls to the function or method func
        """
        def helper(*args, **kwargs):
            helper.calls += 1
            return func(*args, **kwargs)
        helper.calls = 0
        helper.__name__= func.__name__
    
        return helper
    
    
    def __new__(cls, clsname, superclasses, attributedict):
        """ Every method gets decorated with the decorator call_counter,
            which will do the actual call counting
        """
        for attr in attributedict:
            if not callable(attr) and not attr.startswith("__"):
                attributedict[attr] = cls.call_counter(attributedict[attr])
        
        return type.__new__(cls, clsname, superclasses, attributedict)
    
class A(metaclass=FuncCallCounter):
    
    def foo(self):
        pass
    
    def bar(self):
        pass
        
        
if __name__ == "__main__":
    x = A()
    print(x.foo.calls, x.bar.calls)
    x.foo()
    print(x.foo.calls, x.bar.calls)
Wir erhalten folgende Ausgabe:
0 0
1 0