3

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()')
Mushif Ali Nawaz
  • 3,707
  • 3
  • 18
  • 31
Dumb chimp
  • 444
  • 1
  • 4
  • 13

3 Answers3

5

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 the wrapper.count to 0 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 to wrapper.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
Mushif Ali Nawaz
  • 3,707
  • 3
  • 18
  • 31
  • @MadPhysicist fixed – Mushif Ali Nawaz May 14 '20 at 06:46
  • 1
    Also, `hasattr` is more robust than `in __dict__` – Mad Physicist May 14 '20 at 06:56
  • 1
    Nice job addressing all the points. One last nitpick. The analogy with static in C++ is a bit iffy. Python functions are first class objects, instances of FunctionType, and as such can have attributes like any other object. – Mad Physicist May 14 '20 at 07:05
  • @MadPhysicist Yes, you are right. I have explained the concept in context to the use here. It's used like a counter that won't reset after each invocation and would save the previous value. Just like we would do in C++ by defining a `static` variable inside a function. – Mushif Ali Nawaz May 14 '20 at 07:09
  • Disappointing. The guy posted a non-relevant answer. Then took more info from other answers and put it into his answer, and got more upvotes :( – warvariuc May 14 '20 at 09:42
  • @warvariuc Seriously? Can you please tell me what information I have taken from any of the other answers? I have answered all of his questions. – Mushif Ali Nawaz May 14 '20 at 09:49
  • 2
    @warvariuc. Do you really care what his sources were? Or do you care that he got more points than you? – Mad Physicist May 14 '20 at 14:24
  • @MadPhysicist It's about justice. This site motivates people to answer via points. Look at the answer revisions and see that initially it didn't contain anything useful. Then the guy started taking info from other answers (for example comparison to static variables in C/C++). – warvariuc May 21 '20 at 05:26
  • @warvariuc Do you really think I would copy information from your answer to put it into my answer to get upvotes? And if you would like to compare the revisions then you can see the revisions of your answer too. You didn't point out that there was something wrong with `return func` and then you fixed it as I did in my first revision `return func()`, now should I say that you copied mine? I hope you see my point here! – Mushif Ali Nawaz May 21 '20 at 05:42
  • @warvariuc I have just defended my stance here, I don't need to copy information from other's answers to answer mine. In the end, our goal is to help the OP, I answered all of his questions and without looking at any of the other answers. And have a good day! :) – Mushif Ali Nawaz May 21 '20 at 06:16
2

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
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
0

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()')
warvariuc
  • 57,116
  • 41
  • 173
  • 227
  • 1
    Nonlocal wouldn't help here if I understand the purpose correctly – Mad Physicist May 14 '20 at 06:33
  • I think using nonlocal gives the same effect. See the update to the answer. @MadPhysicist – warvariuc May 14 '20 at 06:36
  • I've posted an answer showing why you might want to do it the original way regardless of nonlocal. You can't access the closure it creates easily, making your counter purely for internal use. – Mad Physicist May 14 '20 at 07:01