-1

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.

Toothpick Anemone
  • 4,290
  • 2
  • 20
  • 42
  • I don't understand your question. You don't provide the definition of the `@deftime` decorator, so how can you expect someone to comment on its operation? None of the functions you provide even have the right form to act like a decorator. A decorator takes another function as a parameter value. None of your functions seem to do that. I tried to figure out what you're getting at in the latter part of your question, but I can't figure it out. I don't see any reason why `radius` would ever be "clobbered". You don't show us how `callee` is defined, so it's hard to know what you're thinking. – CryptoFool Sep 17 '22 at 00:52
  • @CryptoFool: Of course they don't provide a definition for `@deftime`. They have no idea how they would define such a decorator. That's why they're asking here. – user2357112 Sep 17 '22 at 00:59
  • 1
    "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." - that is not how scopes work. It is impossible for `callee`'s local variables to shadow anything `lamby` uses. – user2357112 Sep 17 '22 at 01:01
  • I think this is philosophically misguided. Surely it would be better to use modules or classes to organize your functions, so named aren't accidentally overwritten. I assume you aren't trying to prevent intentional overriding, because that's a very useful Python technique. – Tim Roberts Sep 17 '22 at 01:04
  • @user2357112 - ok, what you say kinda makes sense. But to be a question, I think we can and should expect a question of some kind to actually be asked. Is it at all clear to you what this desired decorator is supposed to do? – CryptoFool Sep 17 '22 at 01:04
  • @CryptoFool: Yeah, it's pretty clear. `@deftime` should cause the reference to `LouiseTheLow` inside `HarriettTheHigh` to refer to the binding that existed at definition time, not the second `LouiseTheLow` that was defined after `HarriettTheHigh`. (`@deftime` should also do this for other global variables used by any other functions it might be applied to - nothing specific to `LouiseTheLow` or `HarriettTheHigh` should be hardcoded.) – user2357112 Sep 17 '22 at 01:07
  • (And besides, the post did in fact ask a question. It's in the title.) – user2357112 Sep 17 '22 at 01:08
  • "I have a lot of trouble with lambda functions getting clobbered." I don't understand how this relates to the question. Is this supposed to be a *motivation* for creating such a decorator? Or is it related to a *failed attempt*? Or just what? Rather than speculate about what "might get clobbered", if this is the actual problem then a) it should be in a separate question, and b) it requires a [mre]. – Karl Knechtel Sep 17 '22 at 02:00
  • I tried defining `def callee(l, a, b, c): radius = 0; print(l(a)); print(l(b)); print(l(c))`, and as expected, `radius` does not get "clobbered". – Karl Knechtel Sep 17 '22 at 02:04

2 Answers2

1

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

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • 1
    You could also do a more selective copy of the global namespace, only making new copies of the `func.__code__.co_names` values. But your approach is very clean. – Blckknght Sep 17 '22 at 02:00
  • Right; `co_names` contains names of globals, `co_cellvars` contains names of nonlocals, and `co_varnames` contains names of locals. Very confusing naming scheme, honestly. – Karl Knechtel Sep 17 '22 at 02:07
  • Why make a deep copy? Why not just create a new reference to the original function? @user2357112 will not approve, but I think my answer does the same sort of thing as what yours does but does so more simply, without the need to understand mutliple esoteric aspects of Python – CryptoFool Sep 17 '22 at 02:25
  • Because the global that we want to look up might not be a function. It might be some nested data structure, which might share contents with something else, and thus be impacted if we change a different global. – Karl Knechtel Sep 17 '22 at 02:26
  • @KarlKnechtel - if pigs could fly. There are plenty of other ways an object could change that would not be picked up by a deep copy. There's no way to give a generally correct answer to such an ill defined question. The OP said "other variable names can be used". The issue was obviously one of name clashes, not freezing the current state of an arbitrarily complex data structure. I don't have a problem with your answer, but I think simplicity and understandability is always a plus. The downvotes show others don't agree, but I've been doing this a long time, and simpler is often better. – CryptoFool Sep 17 '22 at 02:34
  • "There are plenty of other ways an object could change that would not be picked up by a deep copy." Yes! The **entire point** is for the change **not** to be picked up! Hence, using the **original** values of the global variables! That's the **point** of the question as asked! "The issue was obviously one of name clashes" There isn't a coherent issue there and I am considering that part of the OP as noise, because as described `callee` **cannot** "clobber" `radius`, per @user2357112's comment. – Karl Knechtel Sep 17 '22 at 02:35
  • I don't think that was the point. The OP stated "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." He says **"variables",** not **"values referred to by variables"**. My solution does exactly what was asked and no more. I just "copy" a variable to preserve its value. .... and again, I never said that your answer was incorrect. My point, which I'm sticking to, is that the simpler solution is easier to understand and may be all that is needed (as it was in the example case provided) – CryptoFool Sep 17 '22 at 02:47
-2

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
CryptoFool
  • 21,719
  • 5
  • 26
  • 44
  • 1
    This hardcodes pretty much everything. The decorator doesn't even use `f` in any way. It's supposed to work for general functions, not just this one exact case. In fact, it's not even passing the right arguments to `LouiseTheLow`, so it relies on the fact that that function ignores its arguments too. – user2357112 Sep 17 '22 at 01:50
  • @user2357112 - why do you need to ride me? I answered what I see as the question being asked, which was nothing more than "How do I bind the first version of the LouiseTheLow function to the wrapped function rather than the second one?". I gave him an elegant answer to that question.I showed how to change the binding of interest to happen at wrapper def time vs wrapper exec time. All of the other stuff you're talking about would need to be better defined in the question to deserve anything more in the answer. The wrapped function isn't called because it would produce output not indicated. – CryptoFool Sep 17 '22 at 02:28