414-1 Decorators: Advanced

Would anybody explain this code from decorators advanced especially the wrapper.count+ = 1 and the wrapper.count = 0 part.
I tried to execute without wrapper.count = 0 and it threw an error message.

def counter(func):
    def wrapper(*args, **kwargs):
        wrapper.count += 1
        # Call the function being decorated and return the result
        return func(*args, **kwargs)

    wrapper.count = 0
    # Return the new decorated function
    return wrapper
@counter
def foo():
    print('calling foo()')    

foo()
foo()

print('foo() was called {} times.'.format(foo.count))

Hey, Aly.

In the preceding mission the author drew a parallel between decorators and nested functions:

image
In order to understand what is going on here, we’ll abandon decorators and look at the version of the code analogous to the one on the code blocks on the left above.

But before we do this, let’s take a quick look at adding attributes to an instance of an object.

Expand to explore this topic

In Python, we sometimes are allowed to add attributes to objects, and sometimes we aren’t:

>>> x = 3
>>> x.aly = "Aly owns this attribute."
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute 'aly'

We got an error — we can’t always do this. But sometimes we can:

>>> def aly_function():
...     return "This is a great function. The best"
>>> aly_function.aly = "Aly owns this attribute."
>>> aly_function.aly
'Aly owns this attribute.'

We won’t get into the details now, suffice to say that you can do this with functions.

Getting back to your question, let’s now create an equivalent version of the given code without decorators:

>>> def counter(func):
...     def wrapper(*args, **kwargs):
...         wrapper.count += 1
...         # Call the function being decorated and return the result
...         return func(*args, **kwargs)
...     wrapper.count = 0
...     # Return the new decorated function
...     return wrapper
>>> def foo():
...     print('calling foo')

Notice that we didn’t add a decorator on top of foo's definition. Also note that there is a line that creates an attribute for wrapper (wrapper.count = 0), just like we saw above.

Now let’s create a new function called new_foo, that will mimic foo when preceded by the decorator @counter — like in the mission

>>> new_foo = counter(foo)

Let’s think about what this function is. It is the result of calling counter with the argument foo. What does counter do? It does three things:

  1. It creates a function wrapper that adds 1 to the value of wrapper.count and returns the result of calling foo. In other words, it creates a function that is pretty much foo, only it adds 1 to the count attribute.

  2. It also creates an attribute in the above function with the value of 0. This will make sure that whenever the line wrapper.count += 1 is ran for the first time, there actually is a value in wrapper.count to which we can add 1.

  3. Finally it returns the wrapper function.

To summarize, counter creates a version of foo with an attribute count initialized with the value 0, with the property that whenever the function is ran, this counter will be incremented by one. Let’s verify this:

>>> new_foo.count
0

As we can see, new_foo.count is 0. Now let’s call this function and recheck the counter:

>>> new_foo()
calling foo
>>> new_foo.count
1

It seems to be working. Let’s call it once again:

>>> new_foo()
calling foo
>>> new_foo.count
2

Yep! It’s working just as described above. One difference between what we did here and the examples, is that we didn’t overwrite foo, but instead created a new function (new_foo). The examples would have worked just the same if we had ran foo = counter(foo) and replaced new_foo with foo everywhere.

I hope this helps.

4 Likes

Hi Bruno,
Wishing you and the dataquest team a happy new year.
Thanks for the clear explanation, it made me understand what is going on.

1 Like

Thanks for typing this out, Bruno. I’m having a hard time wrapping my head around decorators, and this explanation helped. The function attribute (something new to me) threw me for a loop.

1 Like

Hi Bruno/Dataquest Team,

I still can’t understand why when we call the foo function, the wrapper.count attribute does not resets to 0 each time. Does not wrapper.count = 0 run everytime I call foo() ?

Thanks.

Let’s keep the following in mind.

Note the bolded part. Your question in this context is

Does not wrapper.count = 0 run everytime I call new_foo?

When you call counter with foo, you get that object and assign it to new_foo.

At this point new_foo is a new entity in its own right, what counter does won’t impact new_foo at all. You can even delete counter and foo with del counter, foo and new_foo will continue existing.

When you call new_foo, you’re not using counter and foo anymore, it exists by itself.

I hope this helps.

My understanding of nonlocal is not solid enough, so I was thinking why doesn’t wrapper.count+=1 require a nonlocal keyword? My thinking is anything (wrapper in def counter) first defined outside a function (def wrapper) would need nonlocal.

I suspect it’s got to do with this example using attributes and not just normal variables? What edits would have to be made to force this function to require nonlocal?

Another thing is why doesn’t the program error at function definition time? (not yet calling the functions). Error being that when running def counter , i assume python runs top down and defines def wrapper first before reaching wrapper.count=0, so the wrapper.count+=1 inside def wrapper should be undefined?

How do we inspect some useful information about what a function contains? For example, learning something useful by comparing new_foo vs foo