When a function is defined in Python, the variable space is divided into global and local variables. If it is defined in a member function of a class, then there is additional member variable (self) space. So, is there a way to separate these different variable spaces if you want to do it in practice?

Reading and modifying local variables

First, let’s look at reading local variables. There are generally locals(), vars() and sys._getframe(0).f_code.co_varnames, and there is also a method of sys._getframe(0).f_locals, which is actually equivalent to locals() and the related implementation code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
x = 0

class Obj:
    def __init__(self,y):
        self.func(y)
        
    def func(y, z=1):
        m = 2
        print (locals())
        print (vars())
        print (__import__('sys')._getframe(0).f_code.co_varnames)

if __name__ == '__main__':
    Obj(2)

The code runs as follows.

1
2
3
{'self': <__main__.Obj object at 0x7f5cf5e74e50>, 'y': 2, 'z': 1, 'm': 2}
{'self': <__main__.Obj object at 0x7f5cf5e74e50>, 'y': 2, 'z': 1, 'm': 2}
('self', 'y', 'z', 'm')

In the case of the vars method without the specific variable name, it is the equivalent of the locals method, and both return results in dictionary format. If you execute locals or vars under a member function in a class, it comes with a variable of __main__.Obj object, which is equivalent to all the member variables of self, and is actually part of the local variables. And if we use the method co_varnames, then what we get is the names of all the local variables, and we can also define an additional member variable of self in the example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
x = 0

class Obj:
    def __init__(self, y):
        self.p = 5
        self.func(y)

    def func(self, y, z=1):
        m = 2
        print(locals())
        print(vars())
        print(__import__('sys')._getframe(0).f_code.co_varnames)

if __name__ == '__main__':
    Obj(2)
    # {'self': <__main__.Obj object at 0x7fe9aac0ce50>, 'y': 2, 'z': 1, 'm': 2}
    # {'self': <__main__.Obj object at 0x7fe9aac0ce50>, 'y': 2, 'z': 1, 'm': 2}
    # ('self', 'y', 'z', 'm')

As you can see, all the member variables are placed in self. And it should be noted that global variable x is not present in the local variables since the beginning . So since we can isolate local variables, or the names of local variables, in this way, how do we go about adjusting or modifying these local variables? First of all, we need to know that the variable returned by the locals() method is a copy, which means that even if we modify the result returned by the locals method, we can’t really change the value of the local variable itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
x = 0

class Obj:
    def __init__(self,y):
        self.func(y)

    def func(self, y, z=1):
        m = 2
        vars()['z']=2
        locals()['n']=3
        print (locals())
        print (z)

if __name__ == '__main__':
    Obj(2)

In this case, the values of local variables are modified by the vars method and locals method respectively, and the final output is as follows.

1
2
{'self': <__main__.Obj object at 0x7f74d9470e50>, 'y': 2, 'z': 1, 'm': 2, 'n': 3}
1

First of all, we need to explain why the variable n is not printed in this case. As mentioned earlier, the return value of vars and locals is a copy of the real variable, so whether we modify it or add it, the content will not be synchronized to the variable space, that is, the local variable n is still in an undefined state, but only exists in the dictionary of locals or vars. The final printout of z is 1, which means that the value of z is not affected by the change to the vars variable. So is there any way to modify local variables by string (without synchronizing to global variables)? The answer is yes, but the solution is very hacky, see the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import ctypes

x = 0

class Obj:
    def __init__(self,y):
        self.func(y)

    def func(self, y, z=1):
        m = 2
        __import__('sys')._getframe(0).f_locals.update({
            'z': 2,'n': 3
        })
        ctypes.pythonapi.PyFrame_LocalsToFast(
            ctypes.py_object(__import__('sys')._getframe(0)), ctypes.c_int(0))
        print (locals())
        print (z)

if __name__ == '__main__':
    Obj(2)

This case uses the Cython scheme to directly modify the contents of the data frame, and the f_locals used here are essentially locals. After some running, the output is as follows.

1
2
3
{'self': <__main__.Obj object at 0x7fea2e2
a1e80>, 'y': 2, 'z': 2, 'm': 2, 'n': 3}
2

In this case, the local variable z has been successfully modified. However, as mentioned above, even if we modify the value of the local variable in this way, we still cannot create a new local variable with this solution. If you execute print (n), you will still get an error.

Reading and modifying global variables

It’s actually easier to view and modify global variables than it is to modify local variables. Let’s start with an example that shows how to see all global variables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
x = 0

class Obj:
    def __init__(self,y):
        self.func(y)

    def func(self, y, z=1):
        m = 2
        print (globals())

if __name__ == '__main__':
    Obj(2)

There are many ways to get local variables, but getting global variables is usually globals or the equivalent f_globals. the output of the above code is as follows.

1
2
3
4
{'__name__': '__main__', '__doc__': None, '__package__': None,
 '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f202632ac40>,
 '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
 '__file__': 'xxx.py', '__cached__': None, 'x': 0, 'Obj': <class '__main__.Obj'>}

With this approach we find the global variable x, while several local variables within the same function are not shown in the key of globals. And unlike the locals variables, the globals function returns a real data that is directly modifiable and takes effect globally. For example, we define or modify global variables within a function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
x = 0

class Obj:
    def __init__(self,y):
        self.func(y)

    def func(self, y, z=1):
        global m
        m = 2
        globals()['x']=3

if __name__ == '__main__':
    Obj(2)
    print(globals()['x'])
    print(globals()['m'])
    # 3
    # 2

In this example, we can see that not only the modified x value takes effect, but also the new m is synchronized to the global variable. This makes it easier to divide global variables and local variables before assigning or modifying them uniformly.

Reading and modifying member variables

Every defined object in python has a hidden attribute __dict__, which is a dictionary containing the names and values of all member variables. You can use __dict__ to assign values to the member variables of a class, which is very convenient. We can see what is contained in __dict__ with an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
x = 0

class Obj:
    def __init__(self,y):
        self.m = 2
        self.func(y)

    def func(self, y, z=1):
        print (self.__dict__)

if __name__ == '__main__':
    Obj(2)
    # {'m': 2}

As we can see from the output, the __dict__ output is very pure, that is, all the member variable names and variable values. Although member variables are properties of an object, they operate very similarly to global variables globals, unlike locals which are read-only, as shown in the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
x = 0

class Obj:
    def __init__(self,y):
        self.m = 2
        self.func(y)

    def func(self, y, z=1):
        self.m = 5
        self.__dict__['n'] = 6
        print (self.__dict__)
        print (self.m, self.n)

if __name__ == '__main__':
    Obj(2)
    # {'m': 5, 'n': 6}
    # 5
    # 6

In this case, we modified the value of the member variable and also created a new one using __dict__, and you can see that both of them are finally synchronized to the variable space, so that the modification of the member variable is completed.

Summary

Python itself is a flexible and convenient programming language, but convenience can often come with some risks, such as the implementation of built-in functions like exec and eval, which can lead to problems with sandbox escaping. Sometimes we need to batch operations, such as creating or modifying local, global, or member variables, which requires us to first save all variable names as strings, and then call them as variable names when needed. In this article, we introduce a series of non-exec and eval operations (not that there is no risk, also ctype and sys defined data frames are referenced) to view and define and modify the various variables needed.