Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling generated `__init__` in custom `__init__` override on dataclass

Currently I have something like this:

@dataclass(frozen=True)
class MyClass:
  a: str
  b: str
  c: str
  d: Dict[str, str]

...which is all well and good except dicts are mutable, so I can't use my class to key another dictionary.

Instead, I'd like field d to be something like a FrozenSet[Tuple[str, str]], but I'd still like someone constructing an instance of my class to be able to pass a dictionary on the constructor as this is much more intuitive.

So I'd like to do something like

@dataclass(frozen=True)
class MyClass:
  a: str
  b: str
  c: str
  d: FrozenSet[Tuple[str, str]] = field(init=False)

  def __init__(self, a, b, c, d: Dict[str, str]):
    self.original_generated_init(a, b, c)  # ???
    object.setattr(self, 'd', frozenset(d.items()))  # required because my dataclass is frozen

How do I achieve this? Alternatively is there a more elegant way to achieve the same thing?

like image 609
UtterlyConfused Avatar asked Mar 02 '23 22:03

UtterlyConfused


2 Answers

You can use an InitVar and assign to d in __post_init__:

@dataclass(frozen=True)
class MyClass:
  a: str
  b: str
  c: str
  d: FrozenSet[Tuple[str, str]] = field(init=False)
  d_init: InitVar[Dict[str, str]]

  def __post_init__(self, d_init):
    object.__setattr__(self, 'd', frozenset(d_init.items()))
like image 199
a_guest Avatar answered May 03 '23 18:05

a_guest


The answer given by a_guest is correct, and as good as it gets with basic dataclasses, since you always have to work around the fact that they can't support type-validation or -conversion by design. If you want to use either of that cleanly, you have to use a third-party library like attrs, marshmallow, or pydantic.

Just to have something to compare a standardlib-only implementation to, I'm going to show you how your dataclass would look like in pydantic. It's a relatively new framework, and comes with a lot less historical cruft than the other two:

from typing import FrozenSet, Tuple
from pydantic import dataclasses, validator


@dataclasses.dataclass(frozen=True)
class Foo:
    a: str
    b: str
    c: str
    d: FrozenSet[Tuple[str, str]]

    @validator('d', pre=True)
    def d_accepts_dicts(cls, v):
        """Custom validator that allows passing dicts as frozensets.
        
        Setting the 'pre' flag means that it will run before basic type
        validation takes place, e.g. pydantic will not raise a TypeError
        for passing a dict instead of something natively consistent,
        like for example a list, or a frozenset.
        The code itself only checks if the argument passed as 'd' quacks
        like a dict, and transforms it if the answer is 'yes'.
        """
        try:
            return frozenset(v.items())
        except AttributeError:
            return v

There comes some added complexity with installing and using another library, but if you feel regularly enough that your dataclasses need something from the initial list I linked (or pydantic's trademark feature, runtime-type-assertions), it might well be worth it.

like image 40
Arne Avatar answered May 03 '23 16:05

Arne