Python – How to set an attribute in a frozen dataclass custom __init__ method

pythonpython-3.7python-3.xpython-dataclasses

I'm trying to build a @dataclass that defines a schema but is not actually instantiated with the given members. (Basically, I'm hijacking the convenient @dataclass syntax for other purposes). This almost does what I want:

@dataclass(frozen=True, init=False)
class Tricky:
    thing1: int
    thing2: str

    def __init__(self, thing3):
        self.thing3 = thing3

But I get a FrozenInstanceError in the __init__ method:

dataclasses.FrozenInstanceError: cannot assign to field 'thing3'

I need the frozen=True (for hashability). Is there some way I can set a custom attribute in __init__ on a frozen @dataclass?

Best Solution

The problem is that the default __init__ implementation uses object.__setattr__() with frozen classes and by providing your own implementation, you have to use it too which would make your code pretty hacky:

@dataclass(frozen=True, init=False)
class Tricky:
    thing1: int
    thing2: str

    def __init__(self, thing3):
        object.__setattr__(self, "thing3", thing3)

Unfortunately, python does not provide a way to use the default implementation so we can't simply do something like:

@dataclass(frozen=True, init=False)
class Tricky:
    thing1: int
    thing2: str

    def __init__(self, thing3, **kwargs):
        self.__default_init__(DoSomething(thing3), **kwargs)

However, with we can implement that behavior quite easily:

def dataclass_with_default_init(_cls=None, *args, **kwargs):
    def wrap(cls):
        # Save the current __init__ and remove it so dataclass will
        # create the default __init__.
        user_init = getattr(cls, "__init__")
        delattr(cls, "__init__")

        # let dataclass process our class.
        result = dataclass(cls, *args, **kwargs)

        # Restore the user's __init__ save the default init to __default_init__.
        setattr(result, "__default_init__", result.__init__)
        setattr(result, "__init__", user_init)

        # Just in case that dataclass will return a new instance,
        # (currently, does not happen), restore cls's __init__.
        if result is not cls:
            setattr(cls, "__init__", user_init)

        return result

    # Support both dataclass_with_default_init() and dataclass_with_default_init
    if _cls is None:
        return wrap
    else:
        return wrap(_cls)

and then

@dataclass_with_default_init(frozen=True)
class DataClass:
    value: int

    def __init__(self, value: str):
        # error:
        # self.value = int(value)

        self.__default_init__(value=int(value))

Update: I opened this bug and I hope to implement that by 3.9.