Occasionally, you want to do the same thing in Python. For example, this StackOverflow user likes to re-run the same script over and over in the interactive session (e.g., by hitting F5 in the IDE). That's kind of an odd thing to do in general, but it's not hard to imagine cases where it makes sense. For example, you might be expanding or debugging part of the script, and want to use the rest of the script while you do so.
Normally, that wouldn't be a problem, but what if the script and modified created some global variables, or class or function attributes, etc., and you didn't want those to be overwritten?
That might sound like an anti-pattern, but imagine that you have a function that you've memoized with functools.lru_cache, and it's cached hundreds of expensive values. If you replace it with a new copy of the function, it'll have an empty cache.
Of course the right thing to do is to factor out the script into separate modules, and have the script import the stable code instead of just including it directly. But you don't always want to take a break from actively hacking on code to refactor it.
The easy (but ugly) way
You can always do this:@lru_cache() def _square(x): return x*x try: square except NameError: square = _squareAnd if you only have to do it to one function, maybe that's the best answer. But if you have to do it to a dozen functions, that'll get ugly, and all that repetition is an invitation to copy and paste and screw it up somewhere. So, what you want to do is factor it out into a function.
But how? What you want is something like this:
create_if_not_exists(square, _square)In a language like C++, you'd do that by taking a reference to a function variable as the first parameter, but you can't have a reference to the square variable into the function, because that doesn't make any sense in Python; variables aren't things you can take references of.
You might be able to use some horrible frame hacks to pass the value in and have the function figure out the name from the calling frame, but this is already hacky enough. You might be able to do it with MacroPy, but there are probably cooler ways you can solve the original problem once you're using macros.
Strings as names
The key thing to notice is that ultimately, a variable name is just a string that gets looked up in the appropriate scope. Any frame hack, macro, etc. would just be getting the name as a string and setting its value by name anyway, so why not make that explicit?This is one of those examples that shows that, while usually you don't want to dynamically create variables, occasionally you do.
So, how do you do it?
There are three options.
- Use exec to declare the variable global or nonlocal and then reassign it.
- Call setattr on the enclosing scope object.
- Use the globals dict.
First, using exec for reflection is almost always the wrong answer, so let's just rule that out off the bat.
The setattr solution is more flexible, but in this case I think that's actually a negative. The whole point of what we're trying to do is to modify the global scope by (re-)executing a script. If it doesn't work when you instead execfile the script in the middle of a function… good!
The way to create a global variable dynamically is:
def create(name, value): globals()[name] = value
The "if not exists" part
Of course create('square', _square) does the exact same thing as just square = _square. We wanted to only bind square if it doesn't exist, not rebind it no matter what.
Once you think of it as dict value assignment, the answer is obvious:
def create_if_not_exists(name, value): globals().setdefault(name, value)And that's the whole trick.
Decorators
Except it's not the whole trick; there's one more thing we can do: Turn it into a decorator.def create_if_not_exists(name): def wrap(func): globals().setdefault(name, func) return func return wrap
Getting the name for free
Remember when I said that you can't get the name of a variable? Well, a decorator is only going to be called on functions or classes, and almost only ever going to be called on a function or class created with the def or class statement (or the result of decorating such a thing), which means it will have a name built in, as its __name__ attribute.The problem is, this is the name of the actual implementation function, _square, not the name we want to bind it to, square. But those two names should give you an idea: If you just make that underscore prefix a naming convention (and it already fits in with existing Python naming conventions pretty well), you _can_ get the name to bind to. So:
def create_if_not_exists(func): assert func.__name__[0] == '_' globals().setdefault(func.__name__[1:], func) return funcAnd now, all we need to do to prevent the new (decorated) function from overwriting the old one is to attach this decorator:
@create_if_not_exists @lru_cache() def _square(x): return x*xAnd now, we really are done. How much simpler can you get than that?
When you want to rebind square
Maybe square was supposed to be part of the safe, static code that you don't want to blow away with every re-run, but then you found a bug in it. How do you load the new version?Simple: either del square before hitting F5, or square = _square after hitting F5.
A brief discursion on recursion
Renaming functions after they're created doesn't play well with recursive functions. In Python, recursive functions call themselves by global lookup on their own name. So, if you write this:@create_if_not_exists @lru_cache() def _fact(n): if n < 2: return 1 return n * _fact(-1)… your original _fact function is recursively calling whatever happens to be named _fact at run time. Which means that, after a re-run, it's going to be calling the new _fact function, with its new and separate cache, which makes the whole cache thing worthless.
The answer is simple: Call yourself by your public name, not your private name.
@create_if_not_exists @lru_cache() def _fact(n): if n < 2: return 1 return n * fact(-1)Now, your original _fact function, which you've also bound to fact (and, for that matter, the new _fact that you haven't bound to fact) will call fact, which is still the original function. Tada.
Add a comment