Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding how this Python Decorator works

I have been studying how to create your own decorators and the following example was given:

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

# Decorate foo() with the counter() decorator
@counter
def foo():
  print('calling foo()')
  
foo()
foo()

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

I don't understand the logic of that piece of code.

  1. How can I reference a function inside itself (wrapper.count)?
  2. How wrapper has the method count before wrapper is defined?
  3. Shouldn't the line wrapper.count = 0 be executed everytime I call foo()?
like image 279
Federico Vega Avatar asked Oct 15 '22 02:10

Federico Vega


1 Answers

  1. How can I reference a function inside itself (wrapper.count)?

Function bodies are executed only when you call it. By that time, the function is already defined so this is what makes that possible. The following will not give you any error, unless you call it:

>>> def foo():
...     non_existing_function()
...

And whenever you enter the body of foo, foo is already defined, so you can reference it. This is also what makes recursive calls possible.

  1. How wrapper has the method count before wrapper is defined?

The question could also be "How could I increment the wrapper.count before it is initialized?"

But again, we can answer this in the same way: since function bodies are not executed until we call them, wrapper.count is initialized to 0 before wrapper.count += 1.

  1. Shouldn't the line wrapper.count = 0 be executed everytime I call foo()?

Let's look at what is happening. You have written:

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

which is just a syntactic sugar for this:

foo = counter(foo)

Now, we are calling counter function with foo as an argument. What counter does?

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

In human language,

  • define a function named wrapper which takes unknown number of positional and keyword arguments
  • assign 0 as an attribute named count for wrapper function
  • return wrapper to the caller

And when we assign the result back to the foo function, we've actually assigned wrapper to foo. So when we call foo, we are actually calling wrapper. The line wrapper.count = 0 is outside wrapper function so it will not run every time we call foo.

Lastly, I would highly recommend you watching great PyCon talk by Reuven M. Lerner about decorators.

Edit: I didn't read the body of the wrapper, which actually proves that you don't really need to know what is inside the wrapper. My explanations are still correct. But, as it is suggested in @Mark Tolonen's answer, your wrapper should probably return func(*args,**kwargs) not wrapper.count

like image 110
Asocia Avatar answered Nov 20 '22 19:11

Asocia