5

I have a class as follows:

class MyClass(object):
    def __init__(self):
        self.foo = "foo"
        self.bar = "bar"
        self.methodCalls = 0  #tracks number of times any function in the instance is run

    def get_foo(self):
        addMethodCall()
        return self.foo

    def get_bar(self):
        addMethodCall()
        return self.bar

    def addMethodCall(self):
        self.methodCalls += 1

Is there an inbuilt function that is invoked whenever a method is invoked instead of constantly running addMethodCall()?

techydesigner
  • 1,681
  • 2
  • 19
  • 28

1 Answers1

10

No, there are no hooks on a class to do this. Methods are attributes too, albeit somewhat special in that they are produced when accessing the function object on the instance; functions are descriptors.

The call to a method object is then a separate step from producing the method object:

>>> class Foo(object):
...     def bar(self):
...         return 'bar method on Foo'
...
>>> f = Foo()
>>> f.bar
<bound method Foo.bar of <__main__.Foo object at 0x100777bd0>>
>>> f.bar is f.bar
False
>>> stored = f.bar
>>> stored()
'bar method on Foo'

It is the task of the object.__getattribute__() method to invoke the descriptor protocol, so you could hook into that to see when a method is produced, but you'd still need to wrap that produced method object to detect calls. You could return an object with a __call__ method that proxies for the actual method for example.

However, it'd be easier to decorate each method with a decorator that increments a counter every time it is called. Take into account decorators apply to a function before it is bound, so you'll have to pass self along:

from functools import wraps

def method_counter(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        self.methodCalls += 1
        return func(self, *args, **kwargs)
    return wrapper

You'd still need to apply this to all functions in your class. You could apply this manually to all methods you want to count:

class MyClass(object):
    def __init__(self):
        self.foo = "foo"
        self.bar = "bar"
        self.methodCalls = 0  #tracks number of times any function method is run

    @method_counter
    def get_foo(self):
        return self.foo

    @method_counter
    def get_bar(self):
        return self.bar

or you could use a metaclass:

import types

class MethodCounterMeta(type):
    def __new__(mcls, name, bases, body):
        # create new class object
        for name, obj in body.items():
            if name[:2] == name[-2:] == '__':
                # skip special method names like __init__
                continue
            if isinstance(obj, types.FunctionType):
                # decorate all functions
                body[name] = method_counter(obj)
        return super(MethodCounterMeta, mcls).__new__(mcls, name, bases, body)

    def __call__(cls, *args, **kwargs):
        # create a new instance for this class
        # add in `methodCalls` attribute
        instance = super(MethodCounterMeta, cls).__call__(*args, **kwargs)
        instance.methodCalls = 0
        return instance

This takes care of everything the decorator needs, setting a methodCalls attribute for you, so your class doesn't have to:

class MyClass(object):
    __metaclass__ = MethodCounterMeta
    def __init__(self):
        self.foo = "foo"
        self.bar = "bar"

    def get_foo(self):
        return self.foo

    def get_bar(self):
        return self.bar

Demo of the latter approach:

>>> class MyClass(object):
...     __metaclass__ = MethodCounterMeta
...     def __init__(self):
...         self.foo = "foo"
...         self.bar = "bar"
...     def get_foo(self):
...         return self.foo
...     def get_bar(self):
...         return self.bar
...
>>> instance = MyClass()
>>> instance.get_foo()
'foo'
>>> instance.get_bar()
'bar'
>>> instance.methodCalls
2

The above metaclass only considers function objects (so the result of def statements and lambda expressions) part of the class body for decoration. It ignores any other callable objects (there are more types that have a __call__ method, such as functools.partial objects), as are functions added to the class later on.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • i disagree with the no hooks statement, it is perfectly possible to do so – Mixone Jun 15 '16 at 10:40
  • @Mixone: there are no *direct hooks on a class*. You need to understand how methods are created and how calling those objects work instead. – Martijn Pieters Jun 15 '16 at 10:40
  • @Mixone Either way it answers my question perfectly. Also I assume the modifier for special methods is optional? – techydesigner Jun 15 '16 at 10:41
  • @techydesigner: it is; otherwise the `__init__` method is counted too. – Martijn Pieters Jun 15 '16 at 10:42
  • @MartijnPieters ..and if `__init__` was actually called, would it return an error? – techydesigner Jun 15 '16 at 10:43
  • @techydesigner: no, it would not. `__init__` is just another method in this regard. – Martijn Pieters Jun 15 '16 at 10:44
  • No i disagree with the answer in the sense that you state that one cannot hook into it, any method is an attribute and any method is callable, therefore if I call object.__getattribute__ I can get the correct functioning of the internal method and verify if the attribute is callable, a mix of both gives exactly what the questioneer needs in about 5 lines of code – Mixone Jun 15 '16 at 10:44
  • 2
    @Mixone: then *post that as an answer*. But just testing that something is *callable* doesn't mean it'll actually *be* called. – Martijn Pieters Jun 15 '16 at 10:45
  • 2
    @Mixone: So write your solution and post your own answer. – TheLazyScripter Jun 15 '16 at 10:45
  • 3
    @Mixone: thanks for at least posting that as an answer; I'm sorry you felt you had to delete it. The approach did have huge flaws, you were counting attribute access (filtered on callable objects), not calls. In my answer here I did show why that distinction is important. – Martijn Pieters Jun 15 '16 at 10:53
  • I felt like I had to delete it because as you said I forgot to take those things into account ^^ (took long to answer sorry, was busy) – Mixone Jun 15 '16 at 11:37