Access and modify local variables using Python strings

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

1. Read and modify local variables

First, let’s look at the reading of local variables. Generally, there are several methods such as locals(), vars() and sys._getframe(0).f_code.co_varnames. There is also a method of sys._getframe(0).f_locals. In fact, Equivalent to locals(), the relevant implementation code is as follows:

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 result of running this code is as follows:

{<!-- -->'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')

When the vars method does not add a specific variable name, it is equivalent to the locals method, and the results returned by both are in dictionary format. If locals or vars are executed under a member function in a class, a variable of __main__.Obj object will be attached, which is equivalent to all self member variables, which are actually part of local variables. And if you use the co_varnames method, then you get the names of all local variables. We can also define an additional self member variable in the example:

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')

It can be found that all member variables are placed in self. And it should be noted that the global variable x does not appear in the local variables from beginning to end. So since we can separate local variables, or the names of local variables in this way, how do we adjust or modify these local variables? First of all, we need to know that the variable returned by the locals() method is a copy, that is to say, even if the result returned by the locals method is modified, the value of the local variable itself cannot be really changed. This description may be a bit abstract. Let’s take a look at this directly case:

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 through the vars method and the locals method respectively, and the final output is as follows:

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

First of all, we need to explain why the n variable is not printed in this case. As mentioned earlier, the return values of vars and locals are a copy of real variables, so whether we modify or add new ones, the content will not be synchronized to the variables. space, that is to say, the local variable n at this time is still in an undefined state, but only exists in the locals or vars dictionary, and printing at this time will only report a NameError. And the final printout of z is 1, which shows that the value of z is indeed unaffected by the variable modification to vars. So is there any way to modify local variables through strings (not synchronized to global variables)? The answer is yes, but this solution is very hacky, please see the following example:

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)

In this case, Cython’s solution is used to directly modify the content of the data frame, and the f_locals used here are actually locals in essence. After some running, the output is as follows:

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

At this time, the local variable z has been successfully modified, but as mentioned above, even if we modify the value of the local variable through this method, we still cannot create a new local variable through this scheme, and execute it at this time If print (n), there will still be an error message.

2. Read and modify global variables

Compared with modifying local variables, it is actually easier to view and modify global variables. First, let’s use an example to demonstrate how to view all global variables:

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 obtain local variables, but obtaining global variables is generally globals or the equivalent f_globals. The output of the above code execution is as follows:

{<!-- -->'__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'>}

In this way we found the global variable x, and several local variables in the same function are not displayed in the key of globals. Unlike the locals variable, the globals function returns a real data, which can be directly modified and takes effect globally.

For example, we define or modify global variables in functions:

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 find that not only the modified x value takes effect, but also the newly created m is synchronized to the global variable, so that it is easier to divide global variables and local variables and then perform unified assignment or modification.

3. Read and modify member variables

Each defined object in python has a hidden attribute __dict__, which is a dictionary that contains all member variable names and member variable values. In the previous blog, we introduced that it is very convenient to assign values to member variables in the class through __dict__. We can use an example to see what is contained in __dict__:

x = 0
 
class Obj:
    def __init__(self,y):
        self.m = 2
        self. func(y)
 
    def func(self, y, z=1):
        print(self.__dict__)
#Encountered problems in learning and no one answered? The editor created a Python learning exchange group: 711312441
if __name__ == '__main__':
    Obj(2)
    # {'m': 2}

From the output results, we can see that the content output by __dict__ is very pure, that is, all member variable names and variable values. Although the member variable is an attribute of an object, its operation method is very close to the global variable, unlike locals, which are read-only. The specific examples are as follows:

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 used __dict__ to create a new value of the member variable. It can be seen that all of them are synchronized to the variable space in the end, thus completing the modification of the member variable.

4. Summary

Python itself is a relatively flexible and convenient programming language, but convenience is often accompanied by some risks, such as the implementation of built-in functions such as exec and eval, which may lead to the problem of sandbox escaping. And sometimes we need some batch operations, such as batch creation or modification of local, global or member variables, so we need to save all variable names as strings first, and then use them as variables when needed name to call.

In this article, we introduce a series of non-exec and eval operations (not to say that there is no risk, but also refer to the data frame defined by ctype and sys) to view, define and modify the various variables required.