Search code examples
pythonrecursiondecorator

Counting calls and maximum recursive depth by a decorator in python


I would like to have a decorator that has two fields, let's say .ncalls and .rdepth. When it wraps a function, the ncalls should count how many times the function called itself and rdepth should represent maximum recursion depth achieved, if a function is recursive. I've tried something like here (https://stackoverflow.com/a/51690214/12313533) but it doesn't work. My code now looks like a hybrid of a previously mentioned and code to count function calls with a decorator:

def call_counter(func): #a decorator to count calls
    def helper(x):
        helper.calls += 1
        return func(x)
    helper.calls = 0
    return helper

So i need something like this (this doesn't work):

def depthcounter(func):
     def wrapper(*args, **kwargs):
         wrapper.rdepth += 1
         wrapper.ncalls += 1
         result = func(*args, **kwargs)
         return result
     wrapper.ncalls = 0
     wrapper.rdepth = 0
     return wrapper

For example, let's say I have a factorial numbers producing function like

@depthcounter
def factorial(n):
    if n <= 1:
        return 1
    else:
        return n * factorial(n - 1)

#Here is some working code

print(factorial.ncalls, factorial.rdepth)
factorial(4)
print(factorial.ncalls, factorial.rdepth)
factorial(10)
print(factorial.ncalls, factorial.rdepth)
print(factorial.ncalls, factorial.rdepth)
factorial(20)
print(factorial.ncalls, factorial.rdepth)
factorial(2)
print(factorial.ncalls, factorial.rdepth)

And with the decorator I need it to produce this:

0 0
4 4
10 10
10 10
20 20
2 2

But I get this instead:

0 0
4 4
14 14
14 14
34 34
36 36

Solution

  • Decorators are executed as soon as the module is imported, so your assignment wrapper.ncalls = 0 is executed only once and not at every function call. You need to keep track of starting number of counts inside of wrapper function. here is a working piece of code:

    def callcounter(func):
        callcounter.ncalls  = 0
        def wrapper(*args, **kwargs): 
            initial_calls = callcounter.ncalls
            callcounter.ncalls += 1
            result = func(*args, **kwargs)
            wrapper.ncalls = callcounter.ncalls - initial_calls
            return result
        return wrapper
    
    @callcounter
    def factorial(n):
        if n <= 1:
            return 1
        else:
            return n * factorial(n - 1)
    
    factorial(4)
    print(factorial.ncalls)
    factorial(10)
    print(factorial.ncalls)
    

    Output

    4
    10