Reputation: 454
for the function below, I'm trying to understand
i. Why is wrapper.count = 0
initialised below the wrapper function? Why not initialised below def counter(func)? And why doesn't the wrapper.count reset the wrapper.count
to 0 since its ran below the wrapper function?
And I'm trying to understand what is wrapper.count
? Why not just initialise a normal variable count
as opposed to wrapper.count
?
def counter(func):
def wrapper(*args, **kwargs):
wrapper.count += 1
# Call the function being decorated and return the result
return func
wrapper.count = 0
# Return the new decorated function
return wrapper
# Decorate foo() with the counter() decorator
@counter
def foo():
print('calling foo()')
Upvotes: 3
Views: 1812
Reputation: 3866
There is an error with the decorator, inside wrapper function you need to:
return func(*args, **kwargs) # instead of `return func`
Why is
wrapper.count = 0
initialised below the wrapper function?
Because if you do this inside wrapper function then it will always reset the value of wrapper.count
to 0
. Unless you check that it's not already defined. (I have given an example at the end of my answer.)
Why not initialised below
def counter(func)
?
Because the wrapper function is not defined there. So the interpreter will complain about it.
And why doesn't the
wrapper.count
reset thewrapper.count
to0
since it is executed below the wrapper function?
Because this statement is executed only once when you wrap a function with the @counter
decorator and it will not be executed every time you call the foo()
function.
And I'm trying to understand what is
wrapper.count
?
This is a function attribute. More or less similar to static
variables inside functions in C++ etc.
Why not just initialise a normal variable
count
as opposed towrapper.count
?
Because that would be a local variable and it would reset the count
to 0 on each invocation.
There is another way you can define wrapper.count = 0
inside the wrapper function. So now you don't need to define it outside the wrapper
function.
def counter(func):
def wrapper(*args, **kwargs):
if not hasattr(wrapper, 'count'):
wrapper.count = 0
wrapper.count += 1
return func(*args, **kwargs)
return wrapper
Upvotes: 5
Reputation: 114250
At a high level, the decorated function maintains a counter of how many times it was called.
There is one major issue with the code. The wrapper does not actually call the wrapped function as it should. Instead of return func
, which just returns the function object, it should read
return func(*args, **kwargs)
As @warvariuc points out, one possible reason is that the author did not have or did not know about nonlocal
, which lets you access the enclosing namespace.
I think a more plausible reason is that you want to be able to access the counter. Functions are first-class objects with mutable dictionaries. You can assign and access arbitrary attributes on them. It could be convenient to check foo.count
after a few calls, otherwise why maintain it in the first place?
The reason that wrapper.counter
is initialized the way it is is simply that wrapper
does not exist in the local namespace until the def
statement runs to create it. A new function object is made by the inner def
every time you run counter
. def
generally is an assignment that creates a function object every time you run it.
One more minor point about the code you show is that foo.__name__
will be wrapper
instead of foo
after the decoration. To mitigate this, and make it mimic the original function more faithfully, you can use functools.wraps
, which is a decorator for the decorator wrappers. Your code would look like this:
from functools import wraps
def counter(func):
@wraps(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
# Decorate foo() with the counter() decorator
@counter
def foo():
print('calling foo()')
Now you can do
>>> foo.__name__
'foo'
>>> foo()
calling foo()
>>> foo()
calling foo()
>>> foo()
calling foo()
>>> foo.count
3
Upvotes: 2
Reputation: 59594
Why not just initialise a normal variable count as opposed to
variable.count
My guess is that this pattern appeared first in Python 2, where nonlocal
statement was not available. Looks to me the author of the snippet just tries to emulate static variables like in C language ( https://stackoverflow.com/a/279586/248296 ).
Because if you try to use a normal variable declared at top level of function counter
, you would not be able to assign to it inside wrapper
.
If you put count
below counter
you would make it global, so it will be shared among all instances of the decorator, which is possibly no the desired behavior:
count = 0
def counter(func):
def wrapper(*args, **kwargs):
global count
count += 1
return func(*args, **kwargs)
return wrapper
@counter
def foo():
print('calling foo()')
Here is a version with nonlocal
(Python 3+):
def counter(func):
def wrapper(*args, **kwargs):
nonlocal count
count += 1
# Call the function being decorated and return the result
return func(*args, **kwargs)
count = 0
# Return the new decorated function
return wrapper
# Decorate foo() with the counter() decorator
@counter
def foo():
print('calling foo()')
Upvotes: 0