0

So I need some repeated actions within a large function. Aha! nested functions to the rescue! Oh, but most of the repetition involves modification to local variables in the function! Aha! nonlocal to the rescue! But then there is nearly as much nonlocal statement as nested function content. What I really need is a macro?

Hmm. nonlocal * (meaning all variables would be nonlocal references) would be nice, then the nested function could have all its references to the outer scope... but that wouldn't be restricted to the just-outer scope, which could be bad as a general technique. Oh yes, and nonlocal * doesn't exist.

What to do? Hmm. Instead of def, how about compile() some code, and exec() it later?

nestedfunc = compile("some code", "nestedfunc", "exec")

so then later

exec( nestedfunc )

but what about this note from the documentaiton?

Note: The default locals act as described for function locals() below: modifications to the default locals dictionary should not be attempted. Pass an explicit locals dictionary if you need to see effects of the code on locals after function exec() returns.

"some code" really wants to modify local variables in the current scope. Is this going to work?

Nope, even a simple case like some code being

y=y+1

demonstrates the validity of the warning: y is left unchanged in future uses.

Hmm. What if the outer function were also a block of compile'd code, and the locals were passed in to it? That seems to work with cursory testing, the value of y increases with each invocation of the nested function from the compile'd outer function.

a_global = 10
outer_func = compile('''
print( f'{a_global}, {y}')
exec( nested_func )
print( f'{a_global}, {y}')
exec( nested_func )
print( f'{a_global}, {y}')
exec( nested_func )
print( f'{a_global}, {y}')
exec( nested_func )
print( f'{a_global}, {y}')
''', 'outer_func', 'exec')
nested_func = compile('''
global a_global
a_global += 10
y += 1
''', 'nested_func', 'exec')

locs = {'y': 1 }
exec( outer_func, globals(), locs )
exec( outer_func, globals(), locs )
exec( outer_func, globals(), locs )

results:

10, 1
20, 2
30, 3
40, 4
50, 5
50, 5
60, 6
70, 7
80, 8
90, 9
90, 9
100, 10
110, 11
120, 12
130, 13

So this code seems to meet the requirements: nested_func can reference and update local variables in outer_func without neending nonlocal or nonlocal *, cannot (except by using nonlocal or global) access variables in other outer scopes, and is defined only in one place for consistent updates.

Sure is ugly, though. Does anyone see any holes, or have a better solution?

Victoria
  • 497
  • 2
  • 10
  • 20
  • a good solution doesn't use globals in the first place. – Jean-François Fabre Aug 01 '19 at 06:15
  • 1
    This is beyond ugly. It is hard to understand what you try to achieve. If you need to somehow persist inner states of a function for later calls, return the local vars in a dict to the outside and pass them back in again. – Patrick Artner Aug 01 '19 at 06:19
  • 1
    @Jean-FrançoisFabre The use of globals was just to demonstrate the possibility of using global or nonlocal within the nested_func if needed, not a recommendation for using globals. I'm exploring how-to here. – Victoria Aug 01 '19 at 06:43
  • @PatrickArtner The goal is to avoid repeated blocks of identical code in different places within a large function. The example is not particularly interesting, just demonstrating an ugly technique that achieves the goal. Instead of y += 1, substitute a 2-5 line block of code that depends on and alters local state in the function. Instead of simple sequential calls to nested_func, add surrounding conditional logic. The example just distills down to the problem/solution of trying to avoid code repetition. – Victoria Aug 01 '19 at 06:48

1 Answers1

1

Supply what is needed as dictionary of variables to the inner function. No need for globals, no need for exec.

Return the changed state and supply it for further calls:

import random

def some_func(): 
    def senseless(d = None):
        d = d or {}                # empty dict if nothing provided
        k = d.get("k", 42)         # get "local state" from dict for variables 
        u = d.get("u", "0")        #     or use appropriate default values 
        # do something with variables                           
        for i in range(k): 
            print(u if i%2 == 0 else '?', end="")
        print()
        # mutate variable states
        k = random.randint(5,42)
        u = chr(random.randint(0,ord("z")-ord("a"))+ord("a"))
        print(f"next {k} and {u}")

        # return mutated variable states
        return {"k":k, "u":u} 

    # call with defauls, store mutated states
    state = senseless()
    # call with mutated state twice
    state = senseless(state)
    state = senseless(state)
    # call with predefined state
    state = senseless({"k":6,"u":"YeHa"}) 

some_func()

Output:

0?0?0?0?0?0?0?0?0?0?0?0?0?0?0?0?0?0?0?0?0?
next 17 and s
s?s?s?s?s?s?s?s?s
next 14 and s
s?s?s?s?s?s?s?
next 6 and o
YeHa?YeHa?YeHa?
next 26 and y
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • Interesting thought. As coded in your example, it is almost as annoying as proliferated nonlocal statements, and probably requires more lines of code. However, combined with something like https://stackoverflow.com/a/14620633/1265955 the dict could be used in lieu of local variables in the outer_func, and could be passed in to or nonlocal'd by the nested_func, and with a short name like l or lv for the dict, variable references could all be l.goodname and l.descriptivename without using bulky dict syntax. There'd be no need to return modified state, modifications could be done directly. – Victoria Aug 01 '19 at 19:46
  • 1
    I gave this technique (together with the dottable-dict idea in my above comment) a whirl, and split out 4 nested functions from my large function. It works pretty well. The leading lv. isn't too annoying to read or type, ad it definitely saves on nonlocal statements, as well as ensuring that nonlocal doesn't accidentally pick up an outer scope. And truly ephemeral values in the nested functions (and the parent function) can still use truly local variables. – Victoria Aug 02 '19 at 05:25
  • 1
    Oh, and I should have mentioned the lv "dict" of variables is only referenced, so it odesn't need to be passed to the nested functions. All assignments of interest are to items within lv, and that works without needing a nonlocal reference or a passed parameter. One could use this technique for multi-level static scoping, or with a passed parameter, or reference to an outer dict, dynamic scoping as well. Many such use cases are handled fine by the default scoping rules and parameter passing, though, so getting too complicated with this probably isnt̕' practical. Still, it is a tool in the box – Victoria Aug 02 '19 at 06:11