-2

From The number of times a function gets called.

Let's say, this function is called from outside the file, and I don't have access to it. I am building the function, and I just want to be able to count how many times it has been called from some other part of the program.

I can't use the actual code that calls the function. I do not have access to it. I know normally I should, but for testing purposes, I am not supposed to alter the code that calls the function. So I need to count how many times it is being called without accessing the program that calls the function.

I have tried copy and paste into the file with the function being used like so...

def counted_calls(f):
    @functools.wraps(f)
    def count_wrapper(*args, **kwargs):
        count_wrapper.count += 1
        return f(*args, **kwargs)
    count_wrapper.count = 0
    return count_wrapper

def myfunction(a, b, c, d)
    wrapped = counted_calls(counted_calls)
    integrate.quad(wrapped, 0, 1)
    print(wrapped)
    ****more of my code *****

    return something

First I get this error: Unresolved reference 'f'.

But I get this warning:

Expected type 'Union[function, LowLevelCallable]', got '(args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Any' instead

Then when I run it, of course, f is not defined.

How do I get this to work so I can count how many times myfunction() is called?

I can't call my function, another part of the program calls this function whenever it wants to. I just need to track how many times it does. I can't go put this in that part of the program and make it call the function this way. I don't have access to it.
so like this?

def counted_calls(f):
    @functools.wraps(f)
    def count_wrapper(*args, **kwargs):
        count_wrapper.count += 1
        return f(*args, **kwargs)
    count_wrapper.count = 0
    return count_wrapper

wrapped = counted_calls(myfunction)
integrate.quad(wrapped, 0, 1)
print(wrapped)


def myfunction(a, b, c, d)
    code code
return something

This returns the error:

TypeError: myfunction() missing 3 required positional arguments: 
'b', 'c', and 'd'

I tried this, but still wants more arguments or something?

import functools


def counted_calls(f):
    @functools.wraps(f)
    def count_wrapper(*args, **kwargs):
        count_wrapper.count += 1
        return f(*args, **kwargs)
    count_wrapper.count = 0
    return count_wrapper


@counted_calls  # <-- Apply decorator.
def myfunction(a, b, c, d):
    ...
    return something

integrate.quad(myfunction, 0, 1)
print(myfunction.count)
TypeError: myfunction() missing 3 required positional arguments: b, c, d

So I changed it to what was recommended, however, I'm assuming b, c, and d should be something specific, I just don't know what. I tried as recommended:

import functools


def counted_calls(f):
    @functools.wraps(f)
    def count_wrapper(*args, **kwargs):
        count_wrapper.count += 1
        return f(*args, **kwargs)
    count_wrapper.count = 0
    return count_wrapper


@counted_calls  # <-- Apply decorator.
def myfunction(a, b, c, d):
    ...
    return something

integrate.quad(myfunction, 0, 1, 2, 3) # these need to be variables of some 
                                       # kind? Not sure what to put in this 
                                       # line to make it work
print(myfunction.count)

Still missing 2 required positional arguments: 'c' and 'd'.

I hover over the 2 and it says this:

Expected type 'Union[Iterable, tuple, None]', got 'int' instead 
double-beep
  • 5,031
  • 17
  • 33
  • 41
Mind Numb
  • 3
  • 3
  • 1
    The argument to `counted_calls()` should be the function whose calls you want to count, not `counted_calls` itself. – Barmar Mar 10 '21 at 22:55
  • I fixed that in an edit, but still getting errors. see edit. Thank you. – Mind Numb Mar 11 '21 at 16:38
  • Why are you trying to call your function using `integrate.quad`? – Barmar Mar 11 '21 at 16:45
  • Because that is what someone posted below. I'm just trying to follow examples from those suggestions. Is this not how it is supposed to be implemented? – Mind Numb Mar 11 '21 at 18:58
  • That was just a particular example of a function that calls another function -- in the original question they OP wanted to know how many times it calls their function. In your case you should use the actual code that calls your function. – Barmar Mar 11 '21 at 19:13
  • Okay, so that is where the problem is, I can't use the actual code that calls the function. I do not have access to it. I know normally I should, but for testing purposes, I am not supposed to alter the code that calls the function. So I need to count how many times it is being called without accessing the program that calls the function. I'm starting to learn how detailed I need to be in posting questions. Thank you for your help. – Mind Numb Mar 11 '21 at 19:24
  • To call any function, you need to know what arguments to pass it, whether it's been decorated or not. From the "Expected type" information, it looks like _maybe_ the call should have been `integrate.quad(myfunction, (0, 1, 2, 3))` — that's just a guess. It's also unclear how your code that uses a global variable could have worked if the calling sequeuce wasn't `a, b, c, d` separately. – martineau Mar 11 '21 at 19:31
  • Okay, thank you. I have a tutor session tomorrow, so I'll use part of that time to go over these. Thanks again :) – Mind Numb Mar 11 '21 at 19:37

3 Answers3

0

You need to apply the decorator to the function whose calls you want to count by prefixing its name with a @ and putting it on the line before the function to be decorated definition. Afterwards you can use the function's original name to refer to the wrapped version because using the decorator that way is just "syntactic sugar" (see glossary) for:

myfunction = counted_calls(myfunction)

Fixed code:

import functools


def counted_calls(f):
    @functools.wraps(f)
    def count_wrapper(*args, **kwargs):
        count_wrapper.count += 1
        return f(*args, **kwargs)
    count_wrapper.count = 0
    return count_wrapper


@counted_calls  # <-- Apply decorator.
def myfunction(a, b, c, d):
    ...
    return something

integrate.quad(myfunction, 0, 1)
print(myfunction.count)
martineau
  • 119,623
  • 25
  • 170
  • 301
  • Unfortunately, those lines outside the function never get called. I need it to be within the function. I'm starting to think it is impossible. – Mind Numb Mar 11 '21 at 01:56
  • It doesn't matter where the function is called after it's decorated, the calls will be counted. – martineau Mar 11 '21 at 02:00
  • I made an edit to my post to show how I tried to implement but still get that error. would love your feedback on this. thank you. – Mind Numb Mar 11 '21 at 16:40
  • It looks like the number of arguments you're passing the function is incorrect. The `myfunction(a, b, c, d)` in my answer was just an example — to actually run it you would need to use something like `integrate.quad(myfunction, 0, 1, 2, 3)`. You apparently are applying the decorator to a function called `rocket_pid_solution()` which isn't shown in your question. You will need to pass it whatever arguments it requires — applying the decorator to it doesn't change that. – martineau Mar 11 '21 at 16:56
  • Sorry, I changed the function name to reflect the example here. That was the name of the function in the program, which I can not share, but this is how it is structured. The names aren't important. The error still exists, just with relevant names. Please don't focus on the name, it should have been "myfunction" and error is looking for variables b, c and d. Thanks for pointing out the fact that I forgot to change the names in the post. – Mind Numb Mar 11 '21 at 19:04
  • Besides the function name, there's the error message about about "positional arguments: b, c, d" which sure makes it sound like you were running my example code. You really need to be more precise when asking questions here because getting even small details wrong be confusing to others. – martineau Mar 11 '21 at 19:10
  • Okay, I will try harder. Sorry for the confusion. Thank you. – Mind Numb Mar 11 '21 at 19:21
  • I do not yet have the reputation or points to chat, so continuing to post here. Would you be willing to edit your comment to take out (or edit) the function name so that it is not the mistaken function name, please? This really has nothing to do with that program specifically. I don't want unnecessary confusion, as I'm not seeking answers or solutions to the program I'm working on, I'm just trying to find out how to count function calls when I don't have access to the program that is calling it. Thank you. :) – Mind Numb Mar 11 '21 at 19:33
  • Sorry, I can only edit my comments for a minute or two after I make them. Don't worry about it, and good luck. – martineau Mar 11 '21 at 19:36
0

You should be wrapping myfunction. You do this outside the function, not inside the function you want to wrap.

def myfunction(a, b, c, d)
    # ****more of my code *****

    return something

myfunction = counted_calls(myfunction)
# call the other part of your code that uses myfunction
print(myfunction.count)
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • I'm not calling my function, another part of the program is calling my function and I don't have access to change this. The function will be called on time steps, and I just want to count how many times the function is called from the program that uses it. – Mind Numb Mar 11 '21 at 01:36
  • I've upated the answer. You can reassign the function name to contain the wrapped version. – Barmar Mar 11 '21 at 01:38
  • Oh, it's literally a variable that I can store in another name, kinda. But still does not solve the problem. Those lines outside the function never get triggered, do they? The file doesn't run, just the function. – Mind Numb Mar 11 '21 at 01:49
0

You can define the decorator as a class that wraps the function. By implementing the __call__ method of the class, its instance can be called like functions which allows the decorator to simply return the instance as the replacement for the declared function:

class CallCounter:
    def __init__(self,func):
        self.func  = func
        self.count = 0
        
    def __call__(self,*args,**kwargs):
        self.count += 1
        return self.func(*args,**kwargs)

usage:

@CallCounter
def myFunction(a,b,c):
    print(a,b,c)

@CallCounter
def myOtherFunction(x):
    return x*5

for i in range(3):
    myFunction(*map(myOtherFunction,(i,i+i,i*i)))
"""
0 0 0
5 10 5
10 20 20
"""

print(myFunction.count)      # 3
print(myOtherFunction.count) # 9

In order to make it work with classes, there would be two methods to add to the CallCounter class:

    def methodCaller(self,obj):
        def withObject(*args,**kwargs):       
            return self(obj,*args,**kwargs)  # inject object instance
        return withObject

    def __get__(self,obj,objtype=None):   # return method call or CallCounter
        return self.methodCaller(obj) if obj else self 

Within a class, the CallCounter behaves like a property object which is accessed through the __get__ method. The __get__ method can apply to the class or to a specific object instance (obj parameter). When an instance is provided in obj, the CallCounter must inject the object in the method call otherwise there will be a missing parameter. This is why it returns a methodCaller function that captures and inserts the obj parameter. When __get__ pertains to the class itself, then the ClassCounter object is returned directly (which allows access to its count).

usage:

class MyClass:

    @CallCounter
    def method1(self,s):
        print("method1",s)

x = MyClass()
for i in range(3): x.method1(i**3)

print(MyClass.method1.count) # 3
Alain T.
  • 40,517
  • 4
  • 31
  • 51
  • Now I think it's merely another way to implement the original decorator (i.e. class vs function), and doubt the difference will matter — but we'll see. – martineau Mar 11 '21 at 02:52
  • It is indeed merely a different way to implement it but I felt that it better illustrates the inner workings of the decorator. – Alain T. Mar 11 '21 at 02:59