0

I am trying to understand decorators from this link.

I have two simple files a.py and b.py

a.py has the following code:

def f1(input_func):
    def inner_func():
        input_func()
        input_func()
    return inner_func

b.py has the following code:

import a
def saying():
    print('Hello')
saying = a.f1(saying)
saying()

My question is, for the aforementioned instance how does the interpreter know that I am calling the saying object that points to a.f1(saying) and not the object that points to the definition of saying (print('Hello'))?

Bittu
  • 79
  • 1
  • 8

2 Answers2

1

When you make the assignment

saying = a.f1(saying)

from this point on saying points what was evaluated by a.f1(saying), which in this case is the decorated saying function.

If you were to swap the last two lines of py.b then saying() would still call the undecorated saying function.

1

TLDR: You are overwriting the variable name, so the interpreter will simply use the latest definition. Once you run saying = a.f1(saying), there is no longer any way to directly call your original saying unless you have saved it in another variable.

If you want a longer explanation...

Garbage Collection

Normally when you overwrite variables like this, the old value / place in memory is completely erased by the garbage collector. However, in your case the original saying is saved in memory since it is used when calling f1(saying). You just can't access it, again unless you have saved it in another variable before reassigning it.

Let's look at the memory references using the id function to get a better idea of what's going on.

If you run this code:

def f1(input_func):
    def inner_func():
        input_func()
        input_func()
    return inner_func

def saying():
    print('Hello')

print("saying address:", hex(id(saying)))
saying()
print("---")

saying = f1(saying)
print("new saying address = f1(saying):", hex(id(saying)))
saying()

You'll get something like this:

saying address: 0x7fa960295820
Hello
---
new saying address = f1(saying): 0x7fa9602958b0
Hello
Hello

You can see that the two functions are saved in different memory addresses, and saying now points to the new address.

If we save the first address ahead of time, we can use this technique to recover the original function. You would never want to do this in practice (it is infinitely easier to simply save the original saying in a different variable), but for instructional purposes only:

import gc

# ... define f1 and saying as before

def object_by_id(id_):
    for obj in gc.get_objects():
        if id(obj) == id_:
            return obj
    raise Exception("Not found")

origId = id(saying)
print("saying address:", hex(origId))
saying()
print("---")

saying = f1(saying)
print("new saying address = f1(saying):", hex(id(saying)))
saying()
print("---")

saying = object_by_id(origId)
print("saying address after recovery: ", hex(id(saying)))
if id(saying) == origId:
  print( "Matches original!")
saying()

Output:

saying address: 0x7f0c2db4a820
Hello
---
new saying address = f1(saying): 0x7f0c2db4a8b0
Hello
Hello
---
saying address after recovery:  0x7f0c2db4a820
Matches original!
Hello

So you can see that the original function has indeed been recovered. Again, this only works because the new value of saying = f1(saying) contains in its definition a call to the original function.

However, what happens if the call to f1 doesn't include saying, but instead calls another function? Then the original saying is lost permanently. You can see this by adding this snippet to the end of the previous code:

def otherFunc():
  print(1)

saying = f1(otherFunc)
print("new saying address = f1(otherFunc):", hex(id(saying)))
saying()
print("---")

saying = object_by_id(origId) # Exception: Not found

No variables reference the original saying any longer, so the garbage collector removes it and it is no longer recoverable at all.

Demo

jdaz
  • 5,964
  • 2
  • 22
  • 34