
On Tue, Jun 14, 2011 at 12:33 AM, Steven D'Aprano <steve@pearwood.info> wrote:
The problem with injecting locals in the parameter list is that it can only happen at write-time. That's useful, but there's a major opportunity being missed: to be able to inject at runtime. You could add test mocks, optimized functions, logging, turn global variables into local constants, and probably things I've never thought of.
Here's one use-case to give a flavour of what I have in mind: if you're writing Unix-like scripts, one piece of useful functionality is "verbose mode". Here's one way of doing so:
def do_work(args, verbose=False): if verbose: pr = print else: pr = lambda *args: None pr("doing spam") spam() pr("doing ham") ham() # and so on
But why does do_work take a verbose flag? That isn't part of the API for the do_work function itself, which might be usefully called by other bits of code. The verbose argument is only there to satisfy the needs of the user interface. Using a ** hidden argument would solve that problem, but you then have to specify the value of verbose at write-time, defeating the purpose.
Here's an injection solution. First, the body of the function needs a generic hook, with a global do-nothing default:
def hook(*args): pass
def do_work(args): hook("doing spam") spam() hook("doing ham") ham() # and so on
if __name__ == '__main__': if '--verbose' in sys.argv: wrap = inject(hook=print) else: wrap = lambda func: func # do nothing # or `inject(hook=hook)` to micro-optimize wrap(do_work)(my_arguments)
If you want to add logging, its easy: just add an elif clause with wrap = inject(hook=logger).
Because you aren't monkey-patching the hook function (or, heaven help us, monkey-patching builtins.print!) you don't need to fear side-effects. No globals are patched, hence no mysterious action-at-a-distance bugs. And because the injected function is a copy of the original, other parts of the code that use do_work are unaffected.
But for this to work, you have to be able to inject at run-time, not just at write-time.
This is getting deep into major structural changes to the way name lookups work, though. Pre-seeding locals with values that are calculated at run-time is a much simpler concept. A more explicit way to do the same thing might work along the following lines: 1. Add a writeable f_initlocals dict attribute to function objects (None by default) 2. When a function is called, if f_initlocals is not None, use it to initialise the locals() namespace 3. Add a new "local" statement to tell the compiler to treat names as local. Using this statement will create an f_initlocals dict mapping those names to None. 4. Add a new decorator to functools that works like the following: def initlocals(**kwargs): def inner(f): new_names = kwargs.keys() - f.f_initlocals.keys() if new_names: raise ValueError("{} are not local variables of {!r}".format(new_names, f)) f.f_initlocals.update(kwargs) return f return inner @functools.initlocals(mem=collections.Counter()) def do_and_remember(val, verbose=False): local mem result = do_something(val) mem[val] += 1 if verbose: print('Done {} times for {!r}'.format(mem[val], val)) You could still inject changes at runtime with that concept, but would need to be careful with thread-safety issues if you only wanted the change to apply to some invocations and not others. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia