python-3.xcompilationgetattrsetattr

`getattr`, `setattr` and `delattr` restricted to byte-compiled code


I was going through classes doc. And I found the following paragraph.

Notice that code passed to exec() or eval() does not consider the classname of the invoking class to be the current class; this is similar to the effect of the global statement, the effect of which is likewise restricted to code that is byte-compiled together. The same restriction applies to getattr(), setattr() and delattr(), as well as when referencing __dict__ directly.

I have doubts regarding the 'byte-compiled restriction'. I understand exec creates a new environment for code execution where locals and globals are shared to it. And the restriction for global keyword makes sense to me through the following code.

x = 2

def fun():
    exec('global x')
    x = 3
    print("Local - ", x)

fun()
print("Global - ", x)

#Output
Local - 3
Global - 2

The exec statement above will not convert the x inside the function to global.

However, for setattr, getattr and delattr, why it will be affected? Since locals and globals are shared with the exec and getattr and other commands deal will objects, why would they be restricted?

class cl:
    def nest_fun(self):
        exec("setattr(cl, 'var', 2)")
        print(cl.var)

cl().nest_fun()

#Output
2

Maybe there's something in setattr and other commands that I'm missing?


Solution

  • This is about name mangling in class definitions.

    Consider this example :

    class MyClass:
        __class_var = 2  # Mangled to _MyClass__class_var
        exec("__exec_var = 1")  # NOT mangled—sets literal '__exec_var'
    
    print(MyClass.__dict__.keys())
    
    # Output
    # dict_keys(['__module__', '__dict__', ..., '_MyClass__class_var', '__exec_var', ...])
    

    The key __exec_var in MyClass is executed not in the context of the class, so no mangling.

    That's why this code would not work :

    class MyClass:
        __class_var = 2
        def method(self):
            # '__class_var' isn't mangled here
            exec("print(self.__class_var)")
    
    MyClass().method()
    
    Output :
    # AttributeError: 'MyClass' object has no attribute '__class_var'
    

    A direct print(self.__class_var) in the method would be mangled (to _MyClass__class_var) at compile time and work.

    In the end, what about getattr ?

    Consider this code :

    class MyClass:
        __class_var = 2  # Sets '_MyClass__class_var'
    
        def get_direct(self):
            return self.__class_var  # Mangled
    
        def get_via_getattr(self):
            return getattr(self, '__class_var')  # No mangling, AttributeError
    
    obj = MyClass()
    print(obj.get_direct())
    print(obj.get_via_getattr())
    
    # Output
    # 2
    # AttributeError: 'MyClass' object has no attribute '__class_var'
    

    getattr(self, '__class_var') doesn't work,
    however if you followed me, you should now understand that : getattr(self, '_MyClass__class_var') works (but please never do that).

    And last but not least working example code :

    class MyClass:
        __class_var = 2  # Sets '_MyClass__class_var'
        exec("__exec_var=2")
    
        def get_direct(self):
            return self.__class_var  # Mangled
    
        def get_exec_var_via_getattr(self):
            return getattr(self, '__exec_var')
    
    obj = MyClass()
    print(obj.get_direct())
    print(obj.get_exec_var_via_getattr())
    

    I let you guess the output!

    What to remember?

    1. Essentially, these restrictions are here as a "warning" for class definitions involving name mangling. exec (and eval) expressions are not compiled at the same time.
    2. getattr/setattr/delattr are "restricted" similarly because they use runtime strings and bypass mangling entirely as you seen on the examples above. (your example works because it's runtime, non-mangled, and outside the class body)

    bonus : test this code (but never do that please, it's just for the example)

    class MyClass:
        __class_var = 2
        def method(self):
            exec("print(self._MyClass__class_var)")
    
    MyClass().method()