[Python-ideas] 'Injecting' objects as function-local constants
Jim Jewett
jimjjewett at gmail.com
Mon Jun 13 20:11:52 CEST 2011
On Mon, Jun 13, 2011 at 10:33 AM, Steven D'Aprano <steve at pearwood.info> wrote:
> 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.
Even the bind-it-now behavior isn't always for optimization; it can
also be used as a way of forcing stability in case the global name
gets rebound. That is often an anti-pattern in practice, but ... not
always.
>> 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 would say the most obvious place is in a decorator, using the
function object (or a copy) as the namespace. Doing this properly
would require some variant of PEP 3130, which was rejected largely for
insufficient use.
> 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.
Using the function object as a namespace (largely) gets around that,
because you can use a with statement to change the settings
temporarily.
> 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:
[A verbose mode -- full example below, but the new spelling here at the top]
Just replace:
> def do_work(args):
> hook("doing spam")
> spam()
> hook("doing ham")
> ham()
with:
def do_work(args):
__function__.hook("doing spam")
spam()
__function__.hook("doing ham")
ham()
If you want to change the bindings, just rebind do_work.hook to the
correct function. If you are doing this as part of a test, do so
within a with statement that sets it back at the end.
(The reason this requires a variant of 3130 is that the name do_work
may itself be rebound, so do_work.hook isn't a reliable pointer.)
-jJ
[only quotes below here]
> 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.
More information about the Python-ideas
mailing list