Search code examples
pythonimmutabilitypython-dataclasses

Python Frozen Nessted Dataclass, with __call__ = replace


I am trying to write an immutable datastructure with the following charicteristics.

  1. Immutable
  2. Easy to produce immutable copies with altered fields
  3. Composable, so that updateing nested data is as easy as updateing un-nessted data

The API im trying to implement is this

a0 = Person(name = 'Jhon', occupation = {'title': 'junear', 'sallary': 30})
a1 = a(name = a0.name + ' Smith')
a2 = a1(occupation = {'title': 'seanear'})
a3 = a2(occupation = {'sallary': 50})

I have writen an implementation like so


from dataclasses import dataclass, replace, field

@dataclass(frozen=True)
class Occupation:
    __call__ = replace
    title: str
    sallary: int

@dataclass(frozen=True)
class Person:
    __call__ = replace
    name: str
    occupation: Occupation
    
    @property
    def occupation(self):
        return self._occupation

    @occupation.setter
    def occupation(self, value):
        if '_occupation' not in self.__dict__:
            print('initalising occupation')
            occ = Occupation
        else:
            print('updating occupation')
            occ = self.occupation

        if isinstance(value, tuple):
            object.__setattr__(self,'_occupation', occ(*value))
        elif isinstance(value, dict):
            object.__setattr__(self,'_occupation', occ(**value))
        elif isinstance(value, Occupation):
            object.__setattr(self,'_occupation', value)

    

However, Im having problems here. a0 works fine, but the rest fail. I belive the issue is with copying over/ updating the _occupation unmannaged field.

Questions:

  1. Is there a more simple solution to this that I'm over looking
  2. How can I accsess the data from the previouse object within the occupation.setter?
  3. It would be nice if there was a way to generate the boiler plate Ive written, generated when one of the parameters of a frozen dataclass is a frozen dataclass, or even to inline the class definition of the sub-property

Thank you.

Nb:

  1. in writing this code I have pulled from this thread and I have read this documentation

Solution

  • Defining setter for a property kinda breaks your immutability assumption. You need to construct new Occupation, and then create new Person using it.

    
    from dataclasses import dataclass, replace
    
    @dataclass(frozen=True)
    class Occupation:
        __call__ = replace
        title: str
        salary: int
    
    @dataclass(frozen=True)
    class Person:
        name: str
        occupation: Occupation
    
    
        def __call__(self, **kwargs):
            try:
                occupation = kwargs['occupation']
                if isinstance(occupation, tuple):
                    occ = self.occupation(*occupation)
                elif isinstance(occupation, dict):
                    occ = self.occupation(**occupation)
                elif isinstance(occupation, Occupation):
                    occ = occupation
                kwargs['occupation'] = occ
            except KeyError:
                pass
            return replace(self, **kwargs)
    
    
    
    a0 = Person(name = 'John', occupation = Occupation(title= 'junior', salary= 30))
    a1 = a0(name = a0.name + ' Smith')
    a2 = a1(occupation = {'title': 'senior'})
    a3 = a2(occupation = {'salary': 50})