Count Method Calls mit einer Metaklasse


Einführung

Counting Function Calls

Nachdem Sie hoffentlich unser Kapitel Einführung in Metaklassen durchgearbeitet haben, haben Sie sich möglicherweise nach den möglichen Anwendungsfällen für Metaklassen gefragt. Es gibt einige interessante Anwendungsfälle, und es ist nicht - wie manche sagen - eine Lösung, die auf ein Problem wartet. Wir haben bereits einige Beispiele erwähnt.

In diesem Kapitel unseres Tutorials zu Python möchten wir eine beispielhafte Metaklasse erarbeiten, die die Methoden der Unterklasse dekoriert. Die vom Dekorateur zurückgegebene dekorierte Funktion ermöglicht es, zu zählen, wie oft jede Methode der Unterklasse aufgerufen wurde.

Dies ist normalerweise eine der Aufgaben, die wir von einem Profiler erwarten. Daher können wir diese Metaklasse für einfache Profiler verwenden. Natürlich ist es relativ einfach, unsere Metaklasse um weitere Profileranwendungen zu erweitern.



Vorbemerkungen

Bevor wir uns tatsächlich mit dem Problem befassen, möchten wir daran erinnern, wie wir auf die Attribute einer Klasse zugreifen können. Wir werden dies mit der Listenklasse demonstrieren. Mit dem folgenden Konstrukt können wir die Liste aller nicht privaten Attribute einer Klasse - in unserem Beispiel die randomKLasse - abrufen.

import random
cls = "random" # Name der Klasse als String
alle_attribute = [x for x in dir(eval(cls)) if not x.startswith("__") ]
print(alle_attribute)
['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'Random', 'SG_MAGICCONST', 'SystemRandom', 'TWOPI', '_Sequence', '_Set', '_accumulate', '_acos', '_bisect', '_ceil', '_cos', '_e', '_exp', '_inst', '_log', '_os', '_pi', '_random', '_repeat', '_sha512', '_sin', '_sqrt', '_test', '_test_generator', '_urandom', '_warn', 'betavariate', 'choice', 'choices', 'expovariate', 'gammavariate', 'gauss', 'getrandbits', 'getstate', 'lognormvariate', 'normalvariate', 'paretovariate', 'randint', 'random', 'randrange', 'sample', 'seed', 'setstate', 'shuffle', 'triangular', 'uniform', 'vonmisesvariate', 'weibullvariate']

Jetzt filtern wir die aufrufbaren Attribute, d. h. die öffentlichen Methoden der Klasse.

methoden = [x for x in dir(eval(cls)) if not x.startswith("__") 
                              and callable(eval(cls + "." + x))]
print(methoden)
['Random', 'SystemRandom', '_Sequence', '_Set', '_accumulate', '_acos', '_bisect', '_ceil', '_cos', '_exp', '_log', '_repeat', '_sha512', '_sin', '_sqrt', '_test', '_test_generator', '_urandom', '_warn', 'betavariate', 'choice', 'choices', 'expovariate', 'gammavariate', 'gauss', 'getrandbits', 'getstate', 'lognormvariate', 'normalvariate', 'paretovariate', 'randint', 'random', 'randrange', 'sample', 'seed', 'setstate', 'shuffle', 'triangular', 'uniform', 'vonmisesvariate', 'weibullvariate']

Das Abrufen der nicht aufrufbaren Attribute der Klasse kann leicht erreicht werden, indem aufrufbar negiert wird, d. h. not hinzugefügt wird:

nicht_rufbare_attribute = [x for x in dir(eval(cls)) if not x.startswith("__") 
                              and not callable(eval(cls + "." + x))]
print(nicht_rufbare_attribute)
['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'SG_MAGICCONST', 'TWOPI', '_e', '_inst', '_os', '_pi', '_random']

In der normalen Python-Programmierung wird weder empfohlen noch benötigt, Methoden wie folgt anzuwenden, aber es ist möglich:

lst = [3,4]
list.__dict__["append"](lst, 42)
lst
Ausgabe: :

[3, 4, 42]



Bitte beachten Sie die Bemerkung aus der Python-Dokumentation:

"Da dir() in erster Linie als Annehmlichkeit für die Verwendung in einer interaktiven Eingabeaufforderung bereitgestellt wird, versucht es, eine interessante Menge von Namen und nicht eine streng oder konsistent definierten Menge von Namen bereitzustellen, und ihr detailliertes Verhalten kann sich je nach Version ändern. Beispielsweise befinden sich Metaklassenattribute nicht in der Ergebnisliste, wenn das Argument eine Klasse ist."

Ein Dekorateur zum Zählen von Funktionsaufrufen

Nun werden wir mit dem Entwurf der Metaklasse beginnen, die wir am Anfang dieses Kapitels als unser Ziel genannt haben. Es dekoriert alle Methoden seiner Unterklasse mit einem Dekorator, der die Anzahl der Anrufe zählt. Wir haben einen solchen Dekorator in unserem Kapitel Memoization and Decorators 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 es auf die übliche Weise verwenden:

@call_counter
def f():
    pass
print(f.calls)
for _ in range(10):
    f()
    
print(f.calls)
0
10

Es wäre besser, wenn Sie die alternative Notation für die Dekorationsfunktion hinzufügen. Wir werden dies in unserer letzten Metaklasse brauchen:

def f():
    pass
f = call_counter(f)
print(f.calls)
for _ in range(10):
    f()
    
print(f.calls)
0
10



Die Metaklasse "Count Calls"

Jetzt haben wir alle notwendigen "Zutaten" zusammen, um unsere Metaklasse zu schreiben. Wir werden unseren call_counter-Dekorator als statische Methode einbinden:

class FuncCallCounter(type):
    """ Eine Metaklasse, die alle Methoden der 
        Unterklasse mit call_counter als Dekorateur
     """
    
    @staticmethod
    def call_counter(func):
        """ Dekorateur zum Zählen der Funktionsanzahl
             oder Methodenaufrufe an die Funktion oder Methodenfunktion
        """
        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):
        """ Jede Methode wird mit dem Dekorator call_counter dekoriert.
             Dadurch wird die eigentliche Anrufzählung durchgeführt
        """
        for attr in attributedict:
            if callable(attributedict[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)
    x.foo()
    x.bar()
    print(x.foo.calls, x.bar.calls)
        
0 0
1 0
2 1