5

I am trying to derive the default value of id_ from name and vice versa.

@dataclass
class Item:
    id_ = NAME_TO_ID[name]
    name = ID_TO_NAME[id_]

I should be able to call the class like so:

Item(id_=123)
Item(name='foo')

If possible, I would also like the class to raise an error when both id_ and name are provided.

Item(id_=123, name='foo')  # ValueError: id_ and name cannot be provided together

Any recommendations on how I should go about doing this?

Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
skytect
  • 387
  • 3
  • 15

3 Answers3

3

You can use write a __post_init__ method to do these validations

from dataclasses import dataclass, field

@dataclass
class Item:
    id_: int = field(default=None)
    name: str = field(default=None)
    def __post_init__(self):
        if self.id_ is None and self.name is None:
            raise TypeError("You must provide exactly one of name or id_")
        if self.id_ is not None and self.name is not None:
            raise TypeError("You must provide exactly one of name or id_")
        if self.id_ is not None:
            self.name = id_to_name(self.id_)
        else:
            self.id_ = name_to_id(self.name)
Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
  • How is that any better than just a `__init__` with the validation for both fields? We don't need to push dataclasses for everything – JBernardo Oct 16 '18 at 14:35
  • 1
    @JBernardo Because if you have other fields, you can use the generated `__init__` for them. This is a question explicitly about dataclasses – Patrick Haugh Oct 16 '18 at 14:39
  • Was that tag just added? The comment the OP made seemed to imply that he was just trying to see if a dataclass solution makes sense, but in my opinion it's a little overkill. That is just my opinion, however. –  Oct 16 '18 at 14:43
  • Yes (by me). The title was always "How do I access another argument in a default argument in a python dataclass?" though – Patrick Haugh Oct 16 '18 at 14:43
0

Would something as simple as the below work for you?

At object instantiation, check if too much or too little data was provided, then define a property that will compute the value if necessary?

class Item ():
    def __init__(self, id: int =None, name:str= None):
        if all ([name, id]):
            raise ValueError ("id_ and name cannot be provided together")
        elif not any ([name, id]):
            raise ValueError ("name or id must be provided for Item instantiation")
        else:
            self._name = name
            self._id = id
    @property
    def name (self) -> str:
        if self._name is None:
            #Compute the value and return it
            pass #remove this once you figure out your algorithm
        else:
            return self._name
    @property
    def id (self) ->int:
        if self._id is None:
            #Compute the value and return it
            pass #remove this once you figure out your algorithm
       else:
           return self._id

Note that you must also take into consideration what a valid value is. In my provided example case, it is not sufficient if you consider integer 0 a valid id, and an empty string "" a valid name.

0

You'll need to use the __init__ function of your class.

For example,

class Item:
    # define __init__ such that it has a condition when both id_ and name are supplied
    # a ValueError is raised
    def __init__(self, id_, name=None):
        if (id_ and name):
            # raise error because both were supplied
            raise ValueError
        if (id_):
            # assign name and id
        elif (name):
            # assign name and id

Here, though, the user has to pass a value for both. You can simply supply False or None or some falsy value so that it is passed over and the ValueError isn't thrown.

tlm
  • 952
  • 10
  • 20