Search code examples
pythonpython-3.xpython-dataclasses

Is there a way to check if the default values were explicitly passed in to an instance of a dataclass`


Consider the following python code:

from dataclasses import dataclass

@dataclass
class Registration:
  category: str = 'new'

@dataclass
class Car:
  make: str = None
  category: str = None
  reg: Registration = None
 
  def __post_init__(self):
    ''' fill in any missing fields from the registration of car '''
    if self.reg:
      for var in vars(self.reg):
        if not self.var:
          self.var = self.reg.var


r = Registration()
a = Car(make='ford', category='used', reg=r)
# its unknown if b is used/new, so we explicitly pass it None
b = Car(make='ford', category=None, reg=r)

In above example, the __post_init__ is supposed to fill in fields in Car class if it was not passed in during creation of Car object. However if None was explicitly passed in as the field value (in this case for category) it's not supposed to overwrite it from the Registration object. But the above code does. How do I detect what values were explicitly passed in during the object creation vs what are defaults?


Solution

  • I'd be surprised if there were a way to distinguish between a None passed explicitly vs one that the object acquired via its defaults. In situations like yours, one technique is to use a kind sentinel value as the default.

    @dataclass
    class Car:
        NO_ARG = object()
    
        make: str = None
        category: str = NO_ARG
        reg: Registration = None
    
        def __post_init__(self):
            if self.reg:
                for var in vars(self.reg):
                    if getattr(self, var) is self.NO_ARG:
                        setattr(self, var, getattr(self.reg, var))
    

    However, you might also take the awkward situation you find yourself in as a signal that perhaps there's a better way to model your objects. Without knowing more about the broader context it's difficult to offer definitive advice, but I would say that your current strategy strikes me as fishy, so I would encourage you to thinks some more about your OO plan.

    To give one example of an alternative model, rather than using the Registration to overwrite the attributes of a Car, you could instead build a property to expose the Registration attribute when the Car attribute is missing. A user of the class can decide whether they want the category strictly from the Car or they are happy to take the fallback value from the Registration, if available. This approach comes with tradeoffs as well.

    @dataclass
    class Car:
    
        make: str = None
        category: str = None
        reg: Registration = None
    
        @property
        def category_reg(self):
            if self.category is None and self.reg:
                return self.reg.category
            else:
                return self.category