Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python decorators count function call

I'm refreshing my memory about some python features that I didn't get yet, I'm learning from this python tutorial and there's an example that I don't fully understand. It's about a decorator counting calls to a function, here's the code:

def call_counter(func):
    def helper(x):
        helper.calls += 1
        return func(x)
    helper.calls = 0
    return helper

@call_counter
def succ(x):
    return x + 1

if __name__ == '__main__':
    print(succ.calls)
    for i in range(10):
        print(succ(i))
    print(succ.calls)

What I don't get here is why do we increment the calls of the function wrapper (helper.calls += 1) instead of the function calls itself, and why does it actually working?

like image 231
onizukaek Avatar asked Jul 07 '17 10:07

onizukaek


People also ask

How do you count how many times a function has been called Python?

Count() is a Python built-in function that returns the number of times an object appears in a list. The count() method is one of Python's built-in functions. It returns the number of times a given value occurs in a string or a list, as the name implies.

Can Python function have multiple decorators?

Python allows us to implement more than one decorator to a function. It makes decorators useful for reusable building blocks as it accumulates several effects together. It is also known as nested decorators in Python.

How many decorators are there in Python?

In fact, there are two types of decorators in Python — class decorators and function decorators — but I will focus on function decorators here.

How do decorators work in Python?

Decorators dynamically alter the functionality of a function, method, or class without having to directly use subclasses or change the source code of the function being decorated. Using decorators in Python also ensures that your code is DRY(Don't Repeat Yourself).


4 Answers

The important thing to remember about decorators is that a decorator is a function that takes a function as an argument, and returns yet another function. The returned value - yet another function - is what will be called when the name of the original function is invoked.

This model can be very simple:

def my_decorator(fn):
    print("Decorator was called")
    return fn

In this case, the returned function is the same as the passed-in function. But that's usually not what you do. Usually, you return either a completely different function, or you return a function that somehow chains or wraps the original function.

In your example, which is a very common model, you have an inner function that is returned:

def helper(x):
    helper.calls += 1
    return func(x)

This inner function calls the original function (return func(x)) but it also increments the calls counter.

This inner function is being inserted as a "replacement" for whatever function is being decorated. So when your module foo.succ() function is looked up, the result is a reference to the inner helper function returned by the decorator. That function increments the call counter and then calls the originally-defined succ function.

like image 143
aghast Avatar answered Sep 27 '22 19:09

aghast


When you decorate a function you "substitute" you're function with the wrapper.

In this example, after the decoration, when you call succ you are actually calling helper. So if you are counting calls you have to increase the helper calls.

You can check that once you decorate a function the name is binded tho the wrapper by checking the attribute _name_ of the decorated function:

def call_counter(func):
    def helper(*args, **kwargs):
        helper.calls += 1
        print(helper.calls)
        return func(*args, **kwargs)
    helper.calls = 0
    return helper

@call_counter
def succ(x):
    return x + 1

succ(0)
>>> 1
succ(1)
>>> 2
print(succ.__name__)
>>> 'helper'
print(succ.calls)
>>> 2
like image 20
Hrabal Avatar answered Sep 27 '22 19:09

Hrabal


Example with Class Decorator

When you decorate a function with the Class Decorator, every function has its own call_count. This is simplicity of OOP. Every time CallCountDecorator object is called, it will increase its own call_count attribute and print it.

class CallCountDecorator:
    """
    A decorator that will count and print how many times the decorated function was called
    """

    def __init__(self, inline_func):
        self.call_count = 0
        self.inline_func = inline_func

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        self._print_call_count()
        return self.inline_func(*args, **kwargs)

    def _print_call_count(self):
        print(f"The {self.inline_func.__name__} called {self.call_count} times")


@CallCountDecorator
def function():
    pass


@CallCountDecorator
def function2(a, b):
    pass


if __name__ == "__main__":
    function()
    function2(1, b=2)
    function()
    function2(a=2, b=3)
    function2(0, 1)

# OUTPUT
# --------------
# The function called 1 times
# The function2 called 1 times
# The function called 2 times
# The function2 called 2 times
# The function2 called 3 times
like image 33
Mucahit Aktepe Avatar answered Sep 27 '22 18:09

Mucahit Aktepe


What I don't get here is why do we increment the calls of the function wrapper (helper.calls += 1) instead of the function calls itself, and why does it actually working?

I think to make it a generically useful decorator. You could do this

def succ(x):
    succ.calls += 1
    return x + 1

if __name__ == '__main__':
    succ.calls = 0
    print(succ.calls)
    for i in range(10):
        print(succ(i))
    print(succ.calls)

which works just fine, but you would need to put the .calls +=1 in every function you wanted to apply this too, and initialise to 0 before you ran any of them. If you had a whole bunch of functions you wanted to count this is definitely nicer. Plus it initialises them to 0 at definition, which is nice.

As i understand it it works because it replaces the function succ with the helper function from within the decorator (which is redefined every time it decorates a function) so succ = helper and succ.calls = helper.calls. (although of course the name helper is only definied within the namespace of the decorator)

Does that make sense?

like image 23
Stael Avatar answered Sep 27 '22 18:09

Stael