[Python-ideas] 'Injecting' objects as function-local constants
Nick Coghlan
ncoghlan at gmail.com
Tue Jun 14 03:12:05 CEST 2011
On Tue, Jun 14, 2011 at 12:33 AM, Steven D'Aprano <steve at 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 at gmail.com | Brisbane, Australia
More information about the Python-ideas
mailing list