Methodenaufrufe zählen mit einer Metaklasse
Einführung

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 einer 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']Anschließend 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 helperWir können ihn wie gewohnt verwenden:
@call_counter def f(): pass print(f.calls) for _ in range(10): f() print(f.calls)Ausgabe:
0 10Noch 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