1

My goal is to change the scope of a function to a dictionary, instead of where it is defined, so that the function sees the variables in the dictionary.

I found out I was able to do the following:

my_dict = {'x': 1, 'y': 2}

def add_all():
  x + y + z

# reference the functions in the dictionary
my_dict.update({'add_all': add_all})

# append my_dict to __global__ of the function
my_dict['add_all'].__globals__.update(my_dict)

z = 3

my_dict['add_all']() # sees x, y and z
# 6

This works, now I try to make another function that changes the variables in the enclosing scope.

def update_x_y(x, y):
   # Have to explicitly refer to my_dict here
   my_dict.update({'x': x, 'y': y})
   # Must update the __globals__ of add_all() again
   add_all.__globals__.update(my_dict)
   add_all()

my_dict.update({'update_x_y': update_x_y})
my_dict['update_x_y'].__globals__.update(my_dict)

my_dict['update_x_y'](10, 20)
# 33

This also works, but very inelegant and dangerous.

Questions:

  • It looks like __globals__ is a dictionary in which a function will evaluate; What I'm doing with __globals__.update() is merely giving it some new values, so each time something changes in my_dict, I have to update again.

    • Is there a way that I could supplant __globals__ with my_dict, albeit readonly?
  • In the update_x_y() function, I had to explicitly refer to my_dict which is defined in the global scope.

    • Is there a way in functions to refer to variables in the outer scope?
    • nonlocal cannot be used, because the enclosing scope has to be a closure
Siqi Zhang
  • 51
  • 4

1 Answers1

1

We can use a variable injection class to clean up the code

Inspired by

Code

class Inject_Variables:
    """ Injects Named Variables into function scope """
    def __init__(self, **kwargs):
    
      if kwargs:
        self.kwargs = kwargs
      
    def __call__(self, f):
      """
       __call__() is only called
      once, as part of creation of the wrapped function! You can only give
      it a single argument, which is the function object.
      """
      @wraps(f)
      def wrapped_f(*args, **kwargs):
        f_globals = f.__globals__
        saved_values = f_globals.copy() # shallow copy

        # Add Object creation context
        if self.kwargs:
          f_globals.update(self.kwargs)

        # Add Function call context
        if kwargs:
          f_globals.update(kwargs)

        try:
          result = f()
        finally:
          f_glabals = saved_values # Undo changes

        return result

      return wrapped_f

Use Cases

def add_all():
  return x + y + z

Test 1

my_dict = {'x': 1, 'y': 2}

z = 3
# Create add that will use my_dict and z in global scope
scoped_add = Inject_Variables(**my_dict)(add_all)
print('Using global z: ', scoped_add())    # use my_dict values
# Output: Using global z:  6

Test 2

print('Using calling parameter z: ', scoped_add(z=0)) 
# Output: Using calling parameter z:  3

Test 3

# Update dictionary
my_dict = {'x': 2, 'y': 3, 'z': 0}
scoped_add = Inject_Variables(**my_dict)(add_all)  # new scoped add
print('Using dictionary z: ', scoped_add())
# Ouput: Using updated dictionary:  5

Test 4

# Using name parameters
# Define new scope using named parameters
z = 300
scoped_add = Inject_Variables(x = 100, y = 200)(add_all)
print('Using named parameters: ', scoped_add())
# Output: Using named parameters:  600

Test 5

z = 3
@Inject_Variables(x=1, y = 2)
def test_all():
    return x + y + z

print('Using in decorator: ', test_all())
# Output: Using in decorator:  6
DarrylG
  • 16,732
  • 2
  • 17
  • 23
  • This looks wonderful. However, I'm wondering if this is setup can let the function manipulate the data in the dictionary as well, or it only flows one-way? – Siqi Zhang Jun 25 '20 at 14:45
  • @SiqiZhang--seems you could manipulate the data in`__init__` and `__call__`. Depends upon how dynamic you need the manipulations. Also you can easily just create a different function (i.e. scoped_add) with different data. – DarrylG Jun 25 '20 at 14:51