Reputation: 355
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.
wrapper.count
)?Upvotes: 7
Views: 526
Reputation: 6056
- 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.
- 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
.
- 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,
wrapper
which takes unknown number of positional and keyword arguments0
as an attribute named count
for wrapper
functionwrapper
to the callerAnd 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
Upvotes: 4
Reputation: 177600
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.
Upvotes: 1
Reputation: 3305
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/
Upvotes: 1