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

Arnaud Delobelle arnodel at gmail.com
Sat Jun 11 22:47:59 CEST 2011


On 11 Jun 2011, at 14:30, 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...
> 
> Sometimes to keep state using a mutable container:
> 
>    def do_and_remember(val, verbose=False, mem=collections.Counter()):
>        result = do_something(val)
>        mem[val] += 1
>        if verbose:
>            print('Done {} times for {!r}'.format(mem[val], val))
> 
> Sometimes, when creating functions dynamically (making use of nested
> scopes), e.g. to keep some individual function features (usable within
> that functions):
> 
>    def make_my_callbacks(callback_params):
>        my_callbacks = []
>        for params in callback_params:
>            def fun1(*args, _params=params, **kwargs):
>                "...do something with args and params..."
>            def fun2(*args, _params=params, **kwargs):
>                "...do something with args and params..."
>            def fun3(*args, _fun1=fun1, _fun2=fun2, **kwargs):
>                """...do something with args and with functions fun1, fun2,
>                for example pass them as callbacks to other functions..."
>            my_callbacks.append((fun1, fun2, fun3))
>        return my_callbacks
> 
> Sometimes simply to make critical parts of code optimised...
> 
>    def do_it_quickly(fields, _len=len, _split=str.split,
>                      _sth=something):
>        return [_len(f), _split(f), _sth(f) for f in fields]
> 
> ...or even for readability -- keeping function-specific constants within
> the function definition:
> 
>    def check_value(val,
>                    VAL_REGEX=re.compile('^...$'),
>                    VAL_MAX_LEN=38):
>        return len(val) <= VAL_MAX_LEN and VAL_RE.search(val) is not None
> 
> In all that cases (and probably some other too) that technique appears
> to be quite useful.
> 
> 
> == The problem ==
> 
> ...is that it is not very elegant. We add arguments which:
> a) mess up function signatures (both in the code and in auto-generated docs);
> b) can be incidentally overriden (especially when a function has an "open"
>   signature with **kwargs).
> 
> 
> == 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()
>        ...
> or maybe:
>    def do_and_remember(val, verbose=False):
>        inject collections.Counter() as mem
>        ...
> 
> 2. (which personally I would prefer)
> To add `dummy' (or `hidden') keyword arguments, defined after **kwargs
> (and after bare ** if kwargs are not needed; we have already have
> keyword-only arguments after *args or bare *):
> 
>    def do_and_remember(val, verbose=False, **, mem=collections.Counter()):
>        ...
> 
> do_and_remember(val, False, mem='something') would raise TypeError and
> `mem' shoudn not appear in help() etc. as a function argument.
> 
> 3.
> To provide a special decorator, e.g. functools.within:
>    @functools.within(mem=collections.Counter())
>    def do_and_remember(val, verbose=False):
>        ...

That's hard to do as (assuming the function is defined at the global scope), mem will be compiled as a global, meaning that you will have to modify the bytecode.  Oh but this makes me think about something I wrote a while ago (see below).


4. Use closures.

def factory(mem):
     def do_and_remember(val, verbose=False)
         result = do_something(val)
         mem[val] += 1
         if verbose:
             print('Done {} times for {!r}'.format(mem[val], val))         ....
     return do_and_remember
do_and_remember = factory(mem=collections.Counter())

Added bonus: you can create many instances of do_and_remember.


----------

Related to this, here's a "localize" decorator that I wrote some time ago for fun (I think it was from a discussion on this list).  It was for python 2.x (could easily be modified for 3.x I think, it's a matter of adapting the attribute names of the function object).  It "freezes" all non local variables in the function.  It's a hack! It may be possible to adapt it.

def new_closure(vals):
    args = ','.join('x%i' % i for i in range(len(vals)))
    f = eval("lambda %s:lambda:(%s)" % (args, args))
    return f(*vals).func_closure

def localize(f):
    f_globals = dict((n, f.func_globals[n]) for n in f.func_code.co_names)
    f_closure = ( f.func_closure and
                  new_closure([c.cell_contents for c in f.func_closure]) )
    return type(f)(f.func_code, f_globals, f.func_name,
                   f.func_defaults, f_closure)

# Examples of how localize works:

x, y = 1, 2
@localize
def f():
    return x + y

def test():
    acc = []
    for i in range(10):
        @localize
        def pr(): print i
        acc.append(pr)
    return acc

def lambdatest():
    return [localize(lambda: i) for i in range(10)]

# These examples will behave as follows:
>>> f()
3
>>> x = 3
>>> f()
3
>>> pr = test()
>>> pr[0]()
0
>>> pr[5]()
5
>>> l = lambdatest()
>>> l[2]()
2
>>> l[7]()
7
>>> 


-- 
Arnaud




More information about the Python-ideas mailing list