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