I need a way to “inject” names into a function from an outer code block, so they are accessible locally and they don’t need to be specifically handled by the function’s code (defined as function parameters, loaded from *args etc.)
The simplified scenario: providing a framework within which the users are able to define (with as little syntax as possible) custom functions to manipulate other objects of the framework (which are not necessarily global).
Ideally, the user defines
def user_func():
Mouse.eat(Cheese)
if Cat.find(Mouse):
Cat.happy += 1
Here Cat, Mouse and Cheese are framework objects that, for good reasons, cannot be bounded to the global namespace.
I want to write a wrapper for this function to behave like this:
def framework_wrap(user_func):
# this is a framework internal and has name bindings to Cat, Mouse and Cheese
def f():
inject(user_func, {'Cat': Cat, 'Mouse': Mouse, 'Cheese': Cheese})
user_func()
return f
Then this wrapper could be applied to all user-defined functions (as a decorator, by the user himself or automatically, although I plan to use a metaclass).
@framework_wrap
def user_func():
I am aware of the Python 3’s nonlocal keyword, but I still consider ugly (from the framework’s user perspective) to add an additional line:
nonlocal Cat, Mouse, Cheese
and to worry about adding every object he needs to this line.
Any suggestion is greatly appreciated.
The more I mess around with the stack, the more I wish I hadn’t. Don’t hack globals to do what you want. Hack bytecode instead. There’s two ways that I can think of to do this.
1) Add cells wrapping the references that you want into
f.func_closure. You have to reassemble the bytecode of the function to useLOAD_DEREFinstead ofLOAD_GLOBALand generate a cell for each value. You then pass a tuple of the cells and the new code object totypes.FunctionTypeand get a function with the appropriate bindings. Different copies of the function can have different local bindings so it should be as thread safe as you want to make it.2) Add arguments for your new locals at the end of the functions argument list. Replace appropriate occurrences of
LOAD_GLOBALwithLOAD_FAST. Then construct a new function by usingtypes.FunctionTypeand passing in the new code object and a tuple of the bindings that you want as the default option. This is limited in the sense that python limits function arguments to 255 and it can’t be used on functions that use variable arguments. None the less it struck me as the more challenging of the two so that’s the one that I implemented (plus there’s other stuff that can be done with this one). Again, you can either make different copies of the function with different bindings or call the function with the bindings that you want from each call location. So it too can be as thread safe as you want to make it.Note that a whole branch of this code (that relating to
EXTENDED_ARG) is untested but that for common cases, it seems to be pretty solid. I’ll be hacking on it and am currently writing some code to validate the output. Then (when I get around to it) I’ll run it against the whole standard library and fix any bugs.I’ll also probably be implementing the first option as well.