[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