Python 冻结嵌套数据类,带 __call__ = replace
我正在尝试创建一个不可变的数据结构,具有以下特点:
- 不可变
- 可以轻松生成带有修改字段的不可变副本
- 可组合,这样更新嵌套数据就和更新非嵌套数据一样简单
我想实现的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
这个未管理字段上。
我有几个问题:
- 有没有更简单的解决方案是我没有想到的?
- 我该如何在occupation.setter中访问之前对象的数据?
- 如果有办法在一个冻结的数据类的参数是另一个冻结的数据类时,自动生成我写的那些样板代码,或者甚至将子属性的类定义内联,那就太好了。
谢谢。
备注:
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})