How can I set an attribute in a frozen dataclass custom __init__ method? How can I set an attribute in a frozen dataclass custom __init__ method? python-3.x python-3.x

How can I set an attribute in a frozen dataclass custom __init__ method?


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.


I need the frozen=True (for hashability).

There is no strict need to freeze a class just to be hashable. You can opt to just not mutate the attributes from anywhere in your code, and set unsafe_hash=True instead.

However, you should really declare thing3 as a field, and not use a custom __init__:

from dataclasses import dataclass, fieldfrom typing import Any@dataclass(unsafe_hash=True)class Tricky:    thing1: int = field(init=False)    thing2: str = field(init=False)    thing3: Any    def __post_init__(self):        self.thing1 = 42        self.thing2 = 'foo'

Here thing1 and thing2 have init=False set, so they are not passed to the __init__ method. You then set them in a __post_init__() method.

Note that this now requires that you don't freeze the class, otherwise you can't set thing1 and thing2 either, not in a custom __init__ and not in __post_init__.

Demo:

>>> Tricky('bar')Tricky(thing1=42, thing2='foo', thing3='bar')>>> hash(Tricky('bar'))-3702476386127038381

If all you want is a schema definition, you don’t need dataclasses at all. You can get the class annotations from any class; either as raw annotations or with typing.get_type_hints().


Turns out that dataclasses doesn't provide the functionality you were looking for. Attrs however does:

from attr import attrs, attrib@attrs(frozen=True)class Name:    name: str = attrib(converter=str.lower)

Same answer to similar question: See https://stackoverflow.com/a/64695607/3737651