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

Steven D'Aprano steve at pearwood.info
Mon Jun 13 08:23:38 CEST 2011


Terry Reedy wrote:
> On 6/11/2011 9:30 AM, Jan Kaliszewski wrote:
>> == Use cases ==
>>
>> A quite common practice is 'injecting' objects into a function as its
>> locals, at def-time, using function arguments with default values...
[...]
> One problem with trying to 'fix' this is that there can be defaulted args
> which are not intended to be overwritten by users but which are intended to
> be replaced in recursive calls.

I think any solution to this would have to be backward compatible. A big 
NO to anything which changes the behaviour of existing code.


>> == Proposed solutions ==
>>
>> I see three possibilities:
>>
>> 1.
>> To add a new keyword, e.g. `inject':
>>      def do_and_remember(val, verbose=False):
>>          inject mem = collections.Counter()
>>          ...


> The body should all be runtime. Deftime expression should be in the header.


That's not even the case now. The global and nonlocal keywords are in 
the body, and they apply at compile-time.

I don't like the name inject as shown, but I like the idea of injecting 
locals into a function from the outside. (Or rather, into a *copy* of 
the function.) This suggests generalising the idea: take any function, 
and make a copy of it with the specified names/values defined as locals. 
The obvious API is a decorator (presumably living in functools).

Assume we can write such a decorator, and postpone discussion of any 
implementation for now.

Firstly, this provides a way of setting locals at function definition 
time without polluting the parameter list and exposing local variables 
to the caller. Function arguments should be used for arguments, not 
internal implementation details.

@inject(mem=collections.Counter())
def do_and_remember(val, verbose=False):
     # like do_and_remember(val, verbose=False, mem=...)


But more importantly, it has wider applications, like testing, 
introspection, or adding logging to functions:

def my_function(alist):
     return random.choice(alist) + 1

You might not be able to modify my_function, it may be part of a library 
you don't control. As written, if you want to test it, you need to 
monkey-patch the random module, which is a dangerous anti-pattern.

Better to do this:

class randomchoice_mock:
     def choice(self, arg):
         return 0

mock = randomchoice_mock()

test_func = inject(random=mock)(my_function)

Because test_func is a copy of my_function, you can be sure that you 
won't break anything.

Adding logging is just as easy.

This strikes me as the best solution: the decorator is at the head of 
the function, so it looks like a declaration, and it has its effect at 
function definition time. But as Terry points out, such a decorator 
might not be currently possible without language support, or at least 
messy byte-code hacking.




-- 
Steven




More information about the Python-ideas mailing list