Python 冻结嵌套数据类,带 __call__ = replace

0 投票
2 回答
48 浏览
提问于 2025-04-14 15:48

我正在尝试创建一个不可变的数据结构,具有以下特点:

  1. 不可变
  2. 可以轻松生成带有修改字段的不可变副本
  3. 可组合,这样更新嵌套数据就和更新非嵌套数据一样简单

我想实现的API是这样的:

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})

我写了一个实现,像这样:


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)

    

不过,我在这里遇到了一些问题。a0运行得很好,但其他的都失败了。 我认为问题出在复制或更新_occupation这个未管理字段上。

我有几个问题:

  1. 有没有更简单的解决方案是我没有想到的?
  2. 我该如何在occupation.setter中访问之前对象的数据?
  3. 如果有办法在一个冻结的数据类的参数是另一个冻结的数据类时,自动生成我写的那些样板代码,或者甚至将子属性的类定义内联,那就太好了。

谢谢。

备注:

  1. 在写这段代码时,我参考了这个讨论,并且阅读了这份文档

2 个回答

0

这里有一个完整的解决方案,我觉得很不错。
我用了一个自定义的 replace 函数的想法(感谢 @matswecja)。

def myreplace(self,kwargs_= {}, **kwargs):
    kwargs = kwargs_ | kwargs
    current_data = self.__dict__
    updated_data = {}
    sig = self.__annotations__
    for var, arg in kwargs.items():
        if var not in sig:
            raise TypeError(type(self),var)
        typ = sig[var]
        if isinstance(arg, typ):
            updated_data[var] = arg
        elif is_dataclass(typ):
            updated_data[var] = current_data[var](**arg)
        elif callable(arg):
            updated_data[var] = arg(current_data[var])
        else:
            raise TypeError(var, typ, arg, type(arg))
        
    return replace(self, **updated_data)

@dataclass(frozen=True)
class Money:
    __call__ = myreplace
    currency: str
    amount: int
    unit:str = field(default='k')
        

@dataclass(frozen=True)
class Role:
    __call__ = myreplace
    title: str
    salary: Money
    

@dataclass(frozen=True)
class Person:
    __call__ = myreplace
    name: str
    age: int
    role: Role
    

a0 = Person('jhon smith', 26, Role('Jnr',Money('£',20)))
a1 = a0(name=str.title)
a2 = a1(
    {'role': {
        'title' : 'Snr',
        'salary': {
            'amount': lambda a: a + 10
        }}
    })
print(a0)
print(a1)
print(a2)

打印输出:

Person(name='jhon smith', age=26, role=Role(title='Jnr', salary=Money(currency='£', amount=20, unit='k')))
Person(name='Jhon Smith', age=26, role=Role(title='Jnr', salary=Money(currency='£', amount=20, unit='k')))
Person(name='Jhon Smith', age=26, role=Role(title='Snr', salary=Money(currency='£', amount=30, unit='k')))
0

给一个属性定义设置器(setter)会打破你对不可变性的假设。你需要构造一个新的 Occupation,然后再用它创建一个新的 Person


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})

撰写回答