On Mon, Jun 13, 2011 at 10:33 AM, Steven D'Aprano email@example.com wrote:
Nick Coghlan wrote:
On Mon, Jun 13, 2011 at 5:11 PM, Steven D'Aprano firstname.lastname@example.org 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]
def do_work(args): hook("doing spam") spam() hook("doing ham") ham()
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.)
[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.