Home > Blockchain >  How can we create a decorator which will cause a function to use defintion-time variables instead of
How can we create a decorator which will cause a function to use defintion-time variables instead of

Time:09-17

Suppose that we create a decorator named deftime

Consider the following piece of code:

def LouiseTheLow(*ignore, **kw_ignore):
    print("I am Oldy McMold Face")

@deftime
def HarriettTheHigh(*ignored_args, **ignored_keywords):
    LouiseTheLow(59, "some stuff")

###################################################################

def LouiseTheLow(*ignore, **kwignore):
    print("I am The Newest Latest Greatest Thing")

ret_val = HarriettTheHigh() # function call

I expect the above code to print "I am Oldy McMold Face"

Without the decorator, we would see: "I am The Newest Latest Greatest Thing"

Our goal is to write a decorator which causes a function to use the variables which existed when the function was defined, not when the function is called.

Please do not assume that the only variable of interest is named LouiseTheLow.
The decorator should be general enough that other variable names can be used.

I have a lot of trouble with lambda functions getting clobbered.

def caller():
    radius = 10
    lamby = lambda height: height*radius**2
    ret_val = callee(lamby, 1, 2, 3)
    return ret_val

The problem is that callee will often define a local variable which shadows the original variables used inside of the lambda function. In the example above, radius might get clobbered.

CodePudding user response:

Please do not assume that the only variable of interest is named LouiseTheLow.

Rather than try to figure out which variable names to use, let's give the function its own global lookup. (After all, there cannot be any local variables to care about; they don't exist until the function is called.) type allows us to access the object representing the type of functions; like with ordinary classes, calling that can create a function dynamically. It turns out that it accepts two arguments: a code object (representing the compiled code), and a dictionary to use for global lookup.

Thus, we can decorate a function so that it uses global values as they were at decoration time, by cloning the original function with a copy of the original globals:

def deftime(func):
    return type(func)(func.__code__, globals().copy())

Let's test it:

>>> def LouiseTheLow(*ignore, **kw_ignore):
...     print("I am Oldy McMold Face")
... 
>>> 
>>> @deftime
... def HarriettTheHigh(*ignored_args, **ignored_keywords):
...     LouiseTheLow(59, "some stuff")
... 
>>> 
>>> def LouiseTheLow(*ignore, **kwignore):
...     print("I am The Newest Latest Greatest Thing")
... 
>>> 
>>> HarriettTheHigh()
I am Oldy McMold Face

Unfortunately, we cannot easily make a deep copy, because globals() can easily contain values that aren't deep-copyable (in particular, module objects).

CodePudding user response:

I think I get what you're asking. Here's the sort of definition and use of a decorator that you're considering:

def LouiseTheLow(*ignore, **kw_ignore):
    print("I am Oldy McMold Face")

def deftime(f):
    def wrapper(*ignore, **kw_ignore):
        LouiseTheLow(*ignore, **kw_ignore)
    return wrapper

@deftime
def HarriettTheHigh(*ignored_args, **ignored_keywords):
    LouiseTheLow(59, "some stuff")

###################################################################

def LouiseTheLow(*ignore, **kwignore):
    print("I am The Newest Latest Greatest Thing")

ret_val = HarriettTheHigh()  # function call

Here, you would hope that adding the @deftime decorator would cause the first defnition of LouiseTheLow to be bound to the decorated function and called when that function is called. And yet, as maybe you've already surmised, when you run this code, you get the same behavior as without the decorator

I am The Newest Latest Greatest Thing

The problem is that when you attach the decorator to HarrietTheHigh, the wrapper function that is attached creates a closure on the value of LouiseTheLow from the higher scope. So the version of LouiseTheLow that is called by the wrapper will, as you say, be the one that is defined in the outer scope at the time that the wrapper is called. This is just how a closure works. It refers to the variable that it binds to, not the value that this variable had at any particular time.

So, what to do. Well, you want to capture the value of LouiseTheLow at the time that the wrapper is created. To capture it, just assign it to another variable that is also captured by the wrapper function. So long as that variable doesn't change values, it won't matter if a new function with the name LouiseTheLow is defined to replace the original. So here's a new version of the code with that one small change made:

def LouiseTheLow(*ignore, **kw_ignore):
    print("I am Oldy McMold Face")

def deftime(f):
    original_louise = LouiseTheLow
    def wrapper(*ignore, **kw_ignore):
        original_louise(*ignore, **kw_ignore)
    return wrapper

@deftime
def HarriettTheHigh(*ignored_args, **ignored_keywords):
    LouiseTheLow(59, "some stuff")

###################################################################

def LouiseTheLow(*ignore, **kwignore):
    print("I am The Newest Latest Greatest Thing")

ret_val = HarriettTheHigh()  # function call

This produces the output you expect:

I am Oldy McMold Face
  • Related