pythonctypesmagic-methods

get true `__dict__` if `__dict__` was overridden


Is it possible to get object's true __dict__ if __dict__ was overridden? Are there simpler solutions than the ones below?

I came across this example where __dict__ was overridden and got curious. I thought python is using __dict__ to recognize object's attributes but turned out it can be overridden and attributes will still work. So, the original __dict__ is still out there.

class MyClass:
    __dict__ = {}
obj = MyClass()
obj.hacky = 5
# 5, {}
print(obj.hacky, obj.__dict__)

Solution

  • Looking for a solution I've found this. Though it is a bit hacky but it works - it's swapping __class__ to access the original __dict__ and then swapping it back. It works even with __class__, __setattr__ and __dict__ all used for something else.

    def get_true_dict(obj: object) -> dict:
        cls = type(obj)
        newcls = type('',(),{}) # one line empty class
        # __class__ is <attribute '__class__' of 'object' objects>
        object.__dict__['__class__'].__set__(obj, newcls)
        # obj.__class__ = newcls # works only if `__class__` and `__setattr__` are not "bad"
        res = obj.__dict__
        # no safety as class is fine
        obj.__class__ = cls
        return res
    
    # normal
    class X: ...
    x = X()
    x.a = 42
    assert get_true_dict(x) is x.__dict__
    
    # blocked setattr and __dict__ and __class__
    d = {}
    class X:
        __dict__ = d
        __class__ = 42
        __setattr__ = lambda *_: 1/0
    x = X()
    assert x.__dict__ is d
    assert get_true_dict(x) is not x.__dict__
    
    d = get_true_dict(x)
    d['a'] = 42
    assert x.a == 42
    d['b'] = 24
    assert x.b == 24
    print(d)
    

    There is also even more obscure option to get it by using ctypes and offsets but it seems to work only if obj.__dict__ was already accessed before and otherwise returns ValueError: PyObject is NULL. Unless there is some workaround to initialize __dict__, this solution's flaw defies the entire purpose of the get_true_dict method.

    def get_true_dict(obj: object) -> dict:
        import ctypes
        # -24 = -48 + 24
        offset = type(obj).__dictoffset__ + obj.__sizeof__()
        res = ctypes.py_object.from_address(id(obj) + offset)
        return res.value
    
    class X: ...
    x = X()
    x.__dict__ # <<< comment this and it will change the behaviour and will result in
    # ValueError: PyObject is NULL too
    
    x.a = 42
    assert get_true_dict(x) is x.__dict__
    
    d = {}
    class X:
        __dict__ = d
    x = X()
    x.a = 42
    assert x.__dict__ is d
    # ValueError: PyObject is NULL
    assert get_true_dict(x) is not x.__dict__
    d = get_true_dict(x)
    

    PS Code snippets by @denballakh