`frozendict` and `MappingProxyType`s as object-level `__dict__` values

Hi everyone. This is another one of those “frozen/freezable object” thoughts. Most user-defined “immutable” objects are made via subclassing and overriding __setattr__ (or the more convenient @dataclasses.dataclass(frozen=True)). This works well when the object as a whole must be read-only throughout most of its lifetime. As a consequence, internal code that does mutate the object (usually via object.__setattr__()) becomes more verbose and harder to maintain.

Currently, values of the object-level __dict__ attribute are limited to Python dict instances. Allowing users to set other types of mappings such as frozendict and MappingProxyType could be useful for creating frozen objects and read-only proxies for passing data between threads.

An object which is only mutable during initialization:

class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y
        self.__dict__ = frozendict(self.__dict__)

An object that supports creating read-only views:

import types

class Rect:
    def __init__(self, /, x1: int, y1: int, x2: int, y2: int) -> None:
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2

    def __eq__(self, other, /, *, MISSING=object()) -> bool:
        return self.__dict__ == getattr(other, "__dict__", MISSING)

    def as_view(self, /) -> Rect:
        view = self.__new__(__class__)
        view.__dict__ = types.MappingProxyType(self.__dict__)
        return view

rect = Rect(0, 0, 800, 400)
rect_view = rect.as_view()

rect.x2 = 1900
rect.y2 = 1080
assert  rect == rect_view

rect_view.x1 = 600  # error -- mappingproxy is read-only

An object which is only mutable when holding the object’s lock:

import types
import threading

class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y
        self._lock = threading.Lock()
        self._dict = self.__dict__
        self._lock_state()

    def _unlock_state(self):
        self.__dict__ = self._dict

    def _lock_state(self):
        self.__dict__ = types.MappingProxyType(self._dict)

    def __enter__(self):
        self._lock.acquire()
        self._unlock_state()

    def __exit__(self, exc_type=None, exc=None, traceback=None):
        self._lock_state()
        self._lock.release()
        
1 Like

Hi - nice idea! The biggest problem with it, though, is likely to be that (if I remember correctly) the core interpreter relies pretty heavily on __dict__ being an actual dict item, because there are a lot of optimisations that depend on that. Breaking that assumption could therefore be tricky.

Have you looked at how your idea might be implemented? I feel like you could make a much stronger case if you could develop a working prototype.

3 Likes

You actually can assign dict sub-classes to __dict__, however the interpreter does bypass its __setitem__/__getitem__ methods.

class DefiantDict(dict):
    def __setitem__(self, key, value):
        raise RuntimeError("no")

    def __getitem__(self, key):
        raise RuntimeError("no")

class C: pass
c = C()
c.__dict__ = DefiantDict()
c.x = 1  # works
c.x  # 1
vars(c)["x"]  # runtime error: no
1 Like

Cross-linking: a GH issue that proposed this idea

@pf_moore

Have you looked at how your idea might be implemented? I feel like you could make a much stronger case if you could develop a working prototype.

Not yet, but it’s on my to-do list. The GH issue @a-reich linked actually includes a PR for for allowing frozendicts to be used, so maybe I can use that as a starting point. At the very least it will help me familiarize myself with CPython’s inner workings, because I’m throwing myself in the deep end with this :joy: .

@warden

…the interpreter does bypass its __setitem__/__getitem__ methods.

That’s an interesting find. I’m guessing that’s one of those optimizations Paul mentioned.

@a-reich

Cross-linking: a GH issue that proposed this idea

Thanks for the link! A shame the author closed it without further discussion. Their PR might help point me in the right direction regarding where changes need to be made for a prototype.