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

爷,独闯天下 提交于 2020-02-03 21:48:06

问题


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))))
    dispatch))

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":
            reset()
        else:
            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...


回答1:


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.



来源:https://stackoverflow.com/questions/37196577/tracking-number-of-function-calls-closures-%c3%a0-la-sicp-in-python

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!