简体   繁体   中英

Counting how many times a function is called recursively (Python)

I am currently making a program that includes a recursive function that always returns 1. The function looks like this:

def fn(n):
    if n <= 1:
        return n
    elif n > 1 and n % 2 == 0:
        return fn(n/2)
    elif n > 1 and n % 2 > 0:
        return fn(3*n+1)

As I will be creating other functions, I need to create a function that counts how many times fn() is called recursively within the fn() function (How many calls does 'n' take to reach 1). I am able to do this using a global variable, however I am not sure how to do this using another recursive function. Any help would be appreciated. Thanks.

Why not add a second parameter to your function and increment that on recursive calls?

def fn(n):
    def _fn(n, calls):
        if n <= 1:
            return n, calls
        # n > 1 is a given by this point.
        return _fn(n / 2 if n % 2 == 0 else 3 * n + 1, calls + 1)

    return _fn(n, 1)

Use cProfile foo.py:

def fn(n):
    if n <= 1:
        return n
    elif n > 1 and n % 2 == 0:
        return fn(n/2)
    elif n > 1 and n % 2 > 0:
        return fn(3*n+1)


if __name__ == "__main__":
    import sys
    fn(int(sys.argv[1]))

Then execute with:

python -m cProfile foo.py 10
         10 function calls (4 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 foo.py:1(<module>)
      7/1    0.000    0.000    0.000    0.000 foo.py:1(fn)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

This output indicates that fn() is called seven times.

A solution is to add an optional parameter to your function which you can increment at each function's call.

def fn(n, ncalls=1):
    if n <= 1:
        return n, ncalls
    elif n > 1 and n % 2 == 0:
        return fn(n/2, ncalls + 1)
    elif n > 1 and n % 2 > 0:
        return fn(3*n+1, ncalls + 1)

FWIW an arguably simpler option is to hold the count state inside the function itself, without having to nest it nor wrap it in a decorator.

This is akin to doing it thru a global variable, but with the added benefit of restricting the count to the function scope.

For instance:

def fn(n):
    try:
        fn.count += 1
    except AttributeError:
        fn.count = 1

    if n <= 1:
        return n
    elif n > 1 and n % 2 == 0:
        return fn(n/2)
    elif n > 1 and n % 2 > 0:
        return fn(3*n+1)

Output:

In [15]: fn(5)
Out[15]: 1.0

In [16]: fn.count
Out[16]: 6

PS: Your n > 1 check is unnecessary. You might simplify your function a bit by dropping it altogether:

def fn(n):
    try:
        fn.count += 1
    except AttributeError:
        fn.count = 1

    if n <= 1:
        return n
    return fn((3*n + 1) if n % 2 else (n / 2))

Maybe a decorator might be the solution here:

def count(func):
    def counted(value):
        counted.call_count += 1
        return func(value)
    counted.call_count = 0
    return counted

Now, the code would look like

@count
def fn(n):
    if n <= 1:
        return n
    elif n > 1 and n % 2 == 0:
        return fn(n/2)
    elif n > 1 and n % 2 > 0:
        return fn(3*n+1)

(which is just the code but with an extra @count )

fn(n) would return 1 or 1.0, and fn.call_count would return the number of calls.

The simply way:

ncalls=0
def fn(n):
    global ncalls
    ncalls +=1
    if n <= 1:
        return n
    elif n > 1 and n % 2 == 0:
        return fn(n/2)
    elif n > 1 and n % 2 > 0:
        return fn(3*n+1)

if __name__ == "__main__":
    n = 10
    print(f'fn({n}) = {fn(n)}, {ncalls} function call(s)')  

This topic is not so obvious unless you can decorate your function (fully replacing the reference to a function in its definition scope).

If you can't , you better define a function for use with Fixed Point operator , like below:

In [21]: import itertools, functools
    ...: def fn(recurse, n):
    ...:     if n <= 1:
    ...:         return n
    ...:     elif n > 1 and n % 2 == 0:
    ...:         return recurse(n/2)
    ...:     elif n > 1 and n % 2 > 0:
    ...:         return recurse(3*n+1)
    ...:
    ...:
    ...: def fix(f, *args, **kwargs):
    ...:     def g(*args, **kwargs):
    ...:         return f(g, *args, **kwargs)
    ...:     return g
    ...:
    ...: def count_calls(fn, *args, **kwargs):
    ...:     cnt = itertools.count()
    ...:     def wrapper(recurse, *args, **kwargs):
    ...:         cnt.__next__()
    ...:         return fn(recurse, *args, **kwargs)
    ...:     fix(wrapper)(*args, **kwargs)
    ...:     return next(cnt)
    ...:
    ...:
    ...: print(fix(fn)(10))
    ...: count_calls(fn, 10)
1.0
Out[21]: 7

BTW such representation of a recursion allows you to control calls stack or even to translate recursive calls to a loops, great invention:)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM