[Python-ideas] 'Injecting' objects as function-local constants

Steven D'Aprano steve at pearwood.info
Mon Jun 13 16:33:50 CEST 2011


Nick Coghlan wrote:
> On Mon, Jun 13, 2011 at 5:11 PM, Steven D'Aprano <steve at pearwood.info> wrote:
>> Function parameters should be kept for actual arguments, not for optimizing
>> name look-ups.
> 
> Still, the post-** shared state (Jan's option 2) is likely the most
> obvious way to get early binding for *any* purpose without polluting
> the externally visible parameter list. 

I wouldn't call adding even more complexity to function signatures 
"obvious", although I grant that it depends on whether you're Dutch :)

Another disadvantage is that it uses a symbol instead of a word. Too 
many symbols, and your code looks like Perl (or APL). It's hard to 
google for ** to find out what it means. It's harder to talk about a 
symbol than a word. (In written text you can just write ** but in speech 
you have to use circumlocutions or made-up names like double-splat.)

[...]
> It seems like the path of least resistance to me - the prevalence of
> the default argument hack means there's an existing, widespread
> practice that solves real programming issues, but is flawed in some
> ways (specifically, messing with the function's signature). Allowing
> declarations of shared state after the keyword-only arguments seems
> like a fairly obvious answer.

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

if __name__ == '__main__':
     verbose = '--verbose' in sys.argv
     do_work(my_arguments, verbose)

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.



-- 
Steven



More information about the Python-ideas mailing list