-1

I am trying to learn about closures and decorators in python.

I understand that, in my code below, variable fn has been assigned to a 'cell' object, which will itself reference the function object my_func once that is passed as an argument to function "outer" and outer is called.

I do not understand why, when fn is called from within function inner (the decorated function), it calls inner (ie the decorated function) rather than the 'original' undecorated my_func function (which is what was passed as an argument to "outer")

I had not anticipated that decorating my_func would cause the cell which i mentioned above to now reference a "different version" of my_func, which it what it appears from the console output is what has happened.

I therefore have two questions about my code below:

  1. In the assignment my_func = outer(my_func), what is the "my_func" on the left handside of the assignment? I had understood it was a variable called "my_func" which masked the function my_func (but that the original function which was defined as my_func remained).
  2. why does fn now appear to reference inner rather than my_func which is what was passed. Maybe it does not in fact do this and I am misunderstanding something.

Any help would be gratefully appreciated.

I have wrote the following code:

def my_func():
  print(f"This function's name is '{my_func.__name__}'. Its ID is {hex(id(my_func))}")
  return None

def outer(fn):
  def inner():
    print(f"The function that is about to be called is called '{fn.__name__}'. Its ID is {hex(id(fn))}")
    return fn() 
  return inner


print(f"I have created a function called '{my_func.__name__}'. Its ID is {hex(id(my_func))}")

my_func = outer(my_func)

print(f"The function has been decorated. The variable 'my_func' refers to a function object called '{my_func.__name__}'. Its ID is {hex(id(my_func))}\n")

my_func()

The following is printed to the console:

I have created a function called 'my_func'. Its ID is 0x7f3c040cbb50
The function has been decorated. The variable 'my_func' refers to a function object called 'inner'. Its ID is 0x7f3c040f4040

The function that is about to be called is called 'my_func'. Its ID is 0x7f3c040cbb50
This function's name is 'inner'. Its ID is 0x7f3c040f4040

What I was expecting is:

I have created a function called 'my_func'. Its ID is 0x7f3c040cbb50
The function has been decorated. The variable 'my_func' refers to a function object called 'inner'. Its ID is 0x7f3c040f4040

The function that is about to be called is called 'my_func'. Its ID is 0x7f3c040cbb50
The function that is about to be called is called 'my_func'. Its ID is 0x7f3c040cbb50

I recognize that it is my_func = outer(my_func) which is the cause of the 'issue', but I do not understand why. I had expected that the closure fn would continue to reference the "original" undecorated my_func function. I dont understand why assigning my_func to outer(my_func) appears to 'change' the object that fn refers to.

chris
  • 33
  • 3
  • Why do you expect the output of the last line to come from `inner` instead of the original `my_func`? – Mechanic Pig May 03 '23 at 05:31
  • I don't - which is why I am confused! – chris May 03 '23 at 06:03
  • What I mean is that the form of the last line of your expected output is "The function that is about to be called is called {func_name}...", which is the output form in `inner`, isn't it? – Mechanic Pig May 03 '23 at 06:29

1 Answers1

0

When you decorated the my_func function, it was replaced by the inner function.

After being decorated, it is normal to have __my_func__.name == "inner".

To adapt the function name, use the functools.wraps decorator. functools.wraps allows the metadata of the decorated function to be transferred to the enclosing function.

def outer(fn):
    @functools.wraps(fn)
    def inner():
        print(...)
        return fn() 
    return inner

More explanations:

In the statement:

def my_func():
    print(f"Name is '{my_func.__name__}'. ID is {hex(id(my_func))}")
    return None

you create a my_func module variable that holds a reference to a function object. {my_func.__name__} refers to this module variable. Reference pointed by my_func will be resolved each time the function is executed.

my_func = outer(my_func)

After this the variable my_func contains a reference to the newly created inner function. Illustratively, the my_func function has been replaced by the inner function.

When the inner function was created, it captured the my_func reference (closure) as the fn local variable. Reference pointed by fn will never change, and will remain forever the original function.

Each time outer is called, it create a new unique inner function.

When the last print is executed:

print(f"Name is {my_func.__name__}...")

my_func reference the new function so my_func.__name__ is "inner".

In the following example we can see that the reference hosted by my_func changes and the new function has captured a reference to the old one:

>>> def my_func():
...     pass
... 
>>> hex(id(my_func))
'0x7ffb218c0430'
>>> my_func = outer(my_func)
>>> hex(id(my_func))
'0x7ffb218c0310'
>>> my_func.__closure__
(<cell at 0x7ffb218b7c10: function object at 0x7ffb218c0430>,)
Balaïtous
  • 826
  • 6
  • 9
  • Thank you. I specifically didnt use wraps because, simply as part of the process of understanding what was going on, I didn't want to "look through" to the decorated function. Could you elaborate on what you mean by the function being "replaced"? I don't understand how fn "knows" that it should now reference inner. 'my_func' still - seemingly - exists in memory independent of 'inner'. – chris May 03 '23 at 06:32
  • The **function object** that was originally created by `def my_func` still exists in memory indeed - even though the **variable name** `my_func` no longer refers to it. When the f-string inside that function's print statement is evaluated, the variable name `my_func` (as used within braces in the f-string) evaluates to the function object that the variable name `my_func` currently refers to, not the one that the name `my_func` originally referred to. – slothrop May 03 '23 at 20:53
  • Thank you both. I can clearly see that I have used the reference to "my_func" on the second line of my code as if it were a reference to the function object named my_func, rather than the variable my_func (sort of like a 'self' or 'this') - which will not produce the outcome i was expecting. As an aside, is there a way to refer to a function and its attributes from its body? Thank you again – chris May 07 '23 at 21:50
  • *As an aside, is there a way to refer to a function and its attributes from its body?* Not in a super-nice way, but there are things you can try. See: https://stackoverflow.com/questions/4492559/how-to-get-current-function-into-a-variable – slothrop May 08 '23 at 19:30
  • On the other hand, if you don't need the function object, and only want the name that the current function was originally defined with, that's more straightforward: `inspect.stack()[0].function` gives you that name. That would let you get the behaviour you were originally expecting here from your print statements (though I get that the example in your question is a means to an end of learning more about closures, rather than a use case in its own right!) – slothrop May 08 '23 at 19:34