Understanding how this Python Decorator works
Asked Answered
T

3

7

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()?
Toogood answered 25/6, 2020 at 13:29 Comment(1)
In response to point 3, the wrapper function is only called when foo is first decorated, after that, the wrapper function is the only function that wraps foo, so the line doesn't get executedWinifred
H
4
  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

Hartzel answered 25/6, 2020 at 14:29 Comment(0)
N
1

wrapper.count() is just a variable in the wrapper namespace. It is defined outside the wrapper function with wrapper.count = 0 and is executed at the moment the function is decorated. So it is not a method of wrapper(), rather a variable that is global to wrapper(). Each time you call foo(), the wrapper() function is executed, that is, it increases the counter.

You can replace the comment # Call the function ... by the actual call to the function with func() so it shows the print output of foo().

Decorators are not that easy to understand. Here is a link that probably will help you understand what exactly is going on: https://realpython.com/primer-on-python-decorators/

Neckwear answered 25/6, 2020 at 14:10 Comment(0)
F
1

The wrapper is incorrect. return wrapper.count is wrong. As the comment states, it should return the result of the function call with the arguments; otherwise, foo will return the number of times it was called each time instead of its real result.

def counter(func):
    def wrapper(*args, **kwargs):    # "wrapper" function now exists
        wrapper.count += 1           # count doesn't exist yet, but will when wrapper is called.
        return func(*args,**kwargs)  # call the wrapped function and return result
    wrapper.count = 0                # init the attribute on the function
    return wrapper

# Every time "counter" is used, it defines a *different* wrapper function
# with its own localized "count" variable initialized to zero.
@counter
def foo(a,b):
  print(f'foo({a,b})')   # demonstrate that foo was called with the correct arguments.
  return a + b

@counter
def bar(a,b):
  print(f'bar({a,b})')   # demonstrate that bar was called with the correct arguments.
  return a * b

print(foo(1,2))
print(foo(b=3,a=4))     # keywords work, too.
print(bar(5,b=6))

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

Output:

foo((1, 2))
3
foo((4, 3))
7
bar((5, 6))
30
foo() was called 2 times.
bar() was called 1 times.
Fallonfallout answered 25/6, 2020 at 16:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.