Dumb chimp
Dumb chimp

Reputation: 454

Trying to understand a Python wrapper

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

Answers (3)

Mushif Ali Nawaz
Mushif Ali Nawaz

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 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

Upvotes: 5

Mad Physicist
Mad Physicist

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

warvariuc
warvariuc

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

Related Questions