Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to remove dataclass attributes

I have a dataclass like this:

@dataclass
class Bla:
    arg1: Optional[int] = None
    arg2: Optional[str] = None
    arg3: Optional[Dict[str, str]] = None

I want this behavior:

>>> bla = Bla(arg1=None, arg2=None, arg3=None)
>>> asdict(bla)
{'arg1': None, 'arg2': None, 'arg3': None}

>>> bla = Bla()
{}

In this specific case, I could use a dict, but I would lose the possibility of have type-hints (and use mypy)

So I tried this:

class none:
    ...

@dataclass
class Bla:
    arg1: Union[none, int] = none()
    arg2: Union[none, str] = none()
    arg3: Union[none, Dict[str, str]] = none()

    def __post_init__(self) -> None:
        for k, v in self.__dict__.copy().items():
            if isinstance(v, none):
                delattr(self, k)

But the result was:

>>> asdict(Bla())
{'arg1': <__main__.none object at 0x7f71bc0159b0>, 'arg2': <__main__.none object at 0x7f71bc015a90>, 'arg3': <__main__.none object at 0x7f71bc015ac8>}

I expected an empty dict

If try:

>>> a = Bla(None, None, None)
>>> del a.__dict__["arg1"]
>>> asdict(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/(....)/venv/lib/python3.6/site-packages/dataclasses.py", line 1011, in asdict
    return _asdict_inner(obj, dict_factory)
  File "/home/(...)/venv/lib/python3.6/site-packages/dataclasses.py", line 1018, in _asdict_inner
    value = _asdict_inner(getattr(obj, f.name), dict_factory)
AttributeError: 'Bla' object has no attribute 'arg1'

How can I dynamically remove attributes from a dataclass object in a way that I could use asdict after that?

like image 669
GustavoIP Avatar asked Sep 26 '19 15:09

GustavoIP


People also ask

What is @dataclass?

A data class is a class typically containing mainly data, although there aren't really any restrictions. It is created using the new @dataclass decorator, as follows: from dataclasses import dataclass @dataclass class DataClassCard: rank: str suit: str.

What does @dataclass do in Python?

Python introduced the dataclass in version 3.7 (PEP 557). The dataclass allows you to define classes with less code and more functionality out of the box.

Can Dataclass have methods?

A dataclass can very well have regular instance and class methods. Dataclasses were introduced from Python version 3.7. For Python versions below 3.7, it has to be installed as a library.


Video Answer


2 Answers

This isn't an answer to your question about deleting attributes from a dataclass, but it provides a mechanism to get a custom asdict that behaves in the way you want:

from dataclasses import dataclass
from typing import Optional, Dict, cast

SENTINEL = cast(None, object())  # have a sentinel that pretends to be 'None'


@dataclass
class Bla:
    arg1: Optional[int] = SENTINEL
    arg2: Optional[str] = SENTINEL
    arg3: Optional[Dict[str, str]] = SENTINEL

    def asdict(self):
        return {k: v for k, v in self.__dict__.items() if v is not SENTINEL}

Some tests:

>>> Bla().asdict()
{}
>>> Bla(None, None).asdict()
{'arg1': None, 'arg2': None}
>>> Bla(1, 'foo', None).asdict()
{'arg1': 1, 'arg2': 'foo', 'arg3': None}

But remember that it's all a lie, and the attributes do still exist when called explicitly:

>>> print(Bla().arg1)
<object object at 0x7fc89ed84250>
like image 128
Arne Avatar answered Oct 22 '22 02:10

Arne


My question was about how to remove attributes from a dataclasses.dataclass object in a way that I could use the function dataclasses.asdict to generate dictionaries.

My use case was lots of models that I'd like to store in an easy-to-serialize and type-hinted way, but with the possibility of omitting elements (without having any default values). My first approach was to figure out a way to remove any item that I was not explicitly passed in the dataclass constructor.

However, as Patrick and Arne point out, wisely, that's not the right thing to try to accomplish with dataclasses.

The best way I found to solve my use case was to use TypedDict from the typing_extensions module, that, with PEP 589, will be part of the standard library in the module typing in Python 3.8.

In Python 3.8:

from typing import TypedDict, Dict

class Bla(TypedDict):
    arg1: int
    arg2: str
    arg3: Dict[str, str]

In other versions you have to install typing_extensions module:

pip install typing-extensions

then:

from typing_extensions import TypedDict

Let's try:

>>> Bla(arg1=1, arg2="bla", arg3={"bla":"bla"})
{'arg1': 1, 'arg2': 'bla', 'arg3': {'bla': 'bla'}}

>>> Bla(arg1=1, arg2="bla")
{'arg1': 1, 'arg2': 'bla'}

>>> Bla(arg1=1)
{'arg1': 1}

Very elegant and fits perfectly in my use case.

The only drawback is that TypedDict doesn't support default values yet. There is a PR for that and I'm hopeful they will do something about.

like image 25
GustavoIP Avatar answered Oct 22 '22 02:10

GustavoIP