Karthik.K Karthik.K - 8 months ago 33
Python Question

Tracking number of function calls + closures (à la SICP) in Python

This is a question about scope and closures in Python, motivated by an exercise in SICP. Much thanks for your time if you read this!

A question (3.2) in SICP asks one to create a procedure "make-monitored", that takes in a function f (of one parameter) as input and returns a procedure that keeps track of how many times f has been called. (If the input to this new procedure is "num-calls" it returns the number of times f has been called, if it is "reset" it resets counter to 0 and anything else, it applies f to the input and returns the result (after appropriately incrementing the counter).

Here is code in Scheme that I wrote that works:

(define (make-monitored f)
(let ((counter 0))
(define (number-calls) counter)
(define (reset-count)
(set! counter 0))
(define (call-f input)
(begin (set! counter (+ 1 counter))
(f input)))
(define (dispatch message)
(cond ((eq? message 'num-calls) (number-calls))
((eq? message 'reset) (reset-count))
(else (call-f message))))

My question however is about how to write this in a "pythonic" way. My attempt below is obviously a direct translation of my Scheme code and I realize that though it is fine for an impure functional language (like Scheme) it's probably not the cleanest or best way to do it in Python. How does one solve a general problem like this in Python where you want a higher order procedure to dispatch on type and remember local state?

Below is my noobish attempt that works (earlier I had said it did not but the problem was that an earlier version of the program was still in the terminal's memory) (In 2 it seems hard to make nonlocal variable binding)

def make_monitored(func):
counter = 0
def dispatch(message):
if message == "num-calls":
return num_calls()
elif message == "reset":
nonlocal counter
counter += 1
return func(message)
def num_calls():
nonlocal counter
return counter
def reset():
nonlocal counter
counter = 0
return dispatch

PS: This question is related to this same set of exercises in SICP but my question is really about Python best practice and not the concept of closures or Scheme...

phg phg

Besides the question whether the above code works, I'll answer the question "How would one go about solving this type of problem [...] in Python??". I think writing a decorator wrapping the function in a class would be more pythonic:

from functools import wraps 

def make_monitored(func):
    class wrapper:
        def __init__(self, f):
            self.func = f
            self.counter = 0
        def __call__(self, *args, **kwargs):
            self.counter += 1
            return self.func(*args, **kwargs)
    return wraps(func)(wrapper(func))

This has the advantage that it mimics the original function as close as possible, and just adds a counter field to it:

In [25]: msqrt = make_monitored(math.sqrt)
In [26]: msqrt(2)
Out[26]: 1.4142135623730951
In [29]: msqrt.counter
Out[29]: 1
In [30]: msqrt(235)
Out[30]: 15.329709716755891
In [31]: msqrt.counter
Out[31]: 2
In [32]: @make_monitored
    ...: def f(a):
    ...:     """Adding the answer"""
    ...:     return a + 42
In [33]: f(0)
Out[33]: 42
In [34]: f(1)
Out[34]: 43
In [35]: f.counter
Out[35]: 2
In [36]: f.__name__
Out[36]: 'f'
In [37]: f.__doc__
Out[37]: 'Adding the answer'

For f, you also see the usage as a decorator, and how the wrapper keeps the original name and docstring (which would not be the case without functools.wraps).

Defining reset is left as an exercise to the reader, but quite trivial.