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