问题
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
?
回答1:
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.
回答2:
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, field
from 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().
回答3:
What @Shmuel H. posted did not work for me, it still raised FrozenInstanceError
.
This is what worked for me:
What I'm doing here is accepting a value into the init and checking if its compatible with the format that is defined in the strptime
function, if it is I will assign it, if not I will print the exception
@dataclass(frozen=True)
class Birthday:
value: InitVar[str]
date: datetime = field(init=False)
def __post_init__(self, value: str):
try:
self.__dict__['date'] = datetime.strptime(value, '%d/%m/%Y')
except Exception as e:
print(e)
来源:https://stackoverflow.com/questions/57893902/how-can-i-set-an-attribute-in-a-frozen-dataclass-custom-init-method