[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