May Pan
May Pan

Reputation: 3

Trying to Understand this bit of Python code on the topic of decorators

trying to understand this bit of code for some readability and searchability, here is the typed out code

def counter(func):
    def wrapper(*args, **kwargs):
       wrapper.count += 1
       return func (*args, **kwargs)
    wrapper.count = 0 
    return wrapper

@counter
def foo():
   print('calling foo()')

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

currently learning how decorator works...encounter this bit of code couldn't understandwrapper.count = 0 and how when using the decorate in the following code, that the wrapper.count = 2 but not reset to 0

does this have anything to do with closure?

I visited other post which contains similar problem. the answer wasn't that clear to me

can anyone explain the logic behind this code?

Thanks!

Upvotes: 0

Views: 225

Answers (2)

pho
pho

Reputation: 25489

In python, functions are first-class objects, which means that functions themselves are objects, and can be treated as such -- you can assign them to a variable, you can set attributes, etc.

So the line wrapper.count = 0 sets the count attribute for the wrapper object to the integer 0. The fact that wrapper is a function is irrelevant for now.

The way the wrapper function is defined, every time you call wrapper, the function increments its own count attribute.

Now, when you use counter as a decorator, the decorated function is replaced by the value that your decorator returned. It would be as if you did something like this:

def foo():
    print("Calling foo()")

def counter(func):
    def wrapper(*args, **kwargs):
        wrapper.count += 1
        func(*args, **kwargs)
    wrapper.count = 0
    return wrapper

# These two lines are equivalent to decorating foo with @counter   
wrapping_func = counter(foo)
foo = wrapping_func

So when you call foo(), you're actually calling the wrapper function object that was returned when you decorated foo with @counter (i.e. wrapping_func). This wrapper does two things:

  1. Increment its own .count attribute
  2. Call the original foo function with whatever args you passed

Since you called foo() two times, the .count was incremented two times

Later, when you access foo.count, you're actually accessing the count attribute for the wrapper object that we talked about earlier (wrapping_func.count), which is 2.

Upvotes: 1

Elliott
Elliott

Reputation: 39

I haven't used python in a while myself, so I may be a bit rusty, but since there aren't any other answers, I'll see what I can do to help.

Functions in Python are first class, which means they are all actually classes under the hood. Think of def wrapper as declaring a class rather than a function. The only difference is that it only has one method and its constructor is outside the class. wrapper.count = 0 is just serving as that constructor to let Python know that it has an extra property. It does look a little confusing to see that declared after the function though. Now that Python knows about wrapper.count, that property can be accessed in the method. wrapper.count = 0 only runs once (as a constructor), but wrapper.count += 1 runs as many times as you call foo. If there was another function decorated with @counter, it would have a different count than foo's counter because they are different instances of the same class. You can access foo.count because it is not foo anymore. Now it is a separate instance of counter that happens to call foo inside of it.

That may not use the fancy, technically more correct, language, but hopefully that helps.

Upvotes: 0

Related Questions