[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))
      return f
    return inner

   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.


Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia

More information about the Python-ideas mailing list