Alex Alex - 6 days ago 5
Python Question

Function Attributes - Scope

So I have this code:

def collect_input(func):
"""
A decorator which adds an all_input attribute to the wrapped function.
This attribute collects any input passed to the function.
"""
def wrapper(*args, **kwargs):
wrapper.all_input.append(*args)
return func(*args, **kwargs)

wrapper.all_input = []
return wrapper

@collect_input
def foo(bar):
print('in foo')

foo(5)
foo('spam')

print(foo.all_input)


My question is: Why can you access
foo.all_input
, if it is declared in the
collect_input
scope?

Answer

The nice thing of Python is that it has a set of rules to trate its objects, and very little exceptions to these rules.

In this case, the function that is decorated is just a normal Python object. One that happens to be also callable.

What happens in the line wrapper.all_input = [] inside collect_input is that it sets an attribute on the object - at that point named wrapper - but which is the object that will be returned and take the place of the foo function in the global scope. That is the how decorators work.

So lets got trough it step by step to make it as clear as possible:

  1. When running the code above, it defines the collect_input function - which is designed to be used as a decorator.

  2. Then it defines foo function, but before it is added to the global scope, it is passed into the collect_input function. That is what the "@" syntax does. Prior to its existence, the way to decorate functions as to first define a function, and then, replace it for the decorator's return value with a normal assignment. So, the code above is the same as:

    def foo(...): ...

    foo = collect_input(foo)

  3. Inside "collect_input", the original foo func will be called inside the new wrapper function. This wrapper function: a new (function) object created each time the decorator collect_input is called is the object that will take the place of the outermost foo definition. You can see that inside the code for wrapper there is extra code to do exactly what the collect_input is meant to: annotate the input parameters in a list, attached to itself and then resume the call to the original function - foo in this case.

  4. Finally the wrapper object that is returned by collect_input takes the place of foo, but has the all_inputs list attached to it inside the decorator call. So it can be accessed in the global scope as an attribute of the foo object - regardless of where it was defined. Note that you can't, outside the function, use the names func or wrapper, as expected.