You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python冻结嵌套数据类实现疑问:不可变结构与便捷嵌套更新

实现具备嵌套更新能力的不可变数据结构

需求概述

想要实现具备以下特性的不可变数据结构:

  • 不可变性(Immutable)
  • 可轻松生成修改字段后的不可变副本
  • 可组合性,嵌套数据更新与非嵌套数据更新同样便捷

期望的API使用方式:

a0 = Person(name = 'Jhon', occupation = {'title': 'junear', 'sallary': 30})
a1 = a0(name = a0.name + ' Smith')
a2 = a1(occupation = {'title': 'senior'})
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非托管字段的复制/更新逻辑上。


疑问解答

0. 是否存在更简单的解决方案?

有两种可行方向:

  • 轻量自研:给数据类添加自定义__call__方法,自动处理嵌套字段的更新逻辑,将传入的字典转换为子数据类实例并基于原有数据更新。
  • 第三方库:直接使用pyrsistent这类成熟库,它原生支持不可变数据结构和便捷的嵌套更新,API风格与需求匹配,无需手动实现底层逻辑。

1. 如何在occupation的setter中访问前一个对象的数据?

冻结数据类初始化后会禁用__setattr__,setter仅能在实例初始化阶段被调用。你当前的逻辑错误在于:调用a1(occupation={'title': 'senior'})时,实际是通过replace生成新实例,新实例的occupation初始化时_occupation尚未存在,会直接走初始化分支,无法访问原有实例的occupation数据。

正确思路是在生成副本的方法中处理嵌套更新,而非setter。示例代码:

from dataclasses import dataclass, replace

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

    def __call__(self, **kwargs):
        return replace(self, **kwargs)

@dataclass(frozen=True)
class Person:
    name: str
    occupation: Occupation

    def __post_init__(self):
        # 支持字典直接初始化子数据类
        if isinstance(self.occupation, dict):
            object.__setattr__(self, 'occupation', Occupation(**self.occupation))

    def __call__(self, **kwargs):
        # 处理嵌套字段更新
        for key, value in kwargs.items():
            field_type = self.__annotations__[key]
            if hasattr(field_type, '__dataclass_fields__') and isinstance(value, dict):
                original = getattr(self, key)
                kwargs[key] = original(**value)
        return replace(self, **kwargs)

# 测试
a0 = Person(name='Jhon', occupation={'title': 'junear', sallary:30})
a1 = a0(name=a0.name + ' Smith')
a2 = a1(occupation={'title': 'senior'})
a3 = a2(occupation={'sallary':50})
print(a3)  # 输出:Person(name='Jhon Smith', occupation=Occupation(title='senior', sallary=50))

2. 能否自动生成这类样板代码?

可以,通过以下方式实现自动化:

  • 自定义装饰器:编写装饰器遍历数据类字段,自动注入__post_init__(处理字典转数据类)和__call__(处理嵌套更新)逻辑,无需每个类重复编写样板代码。
  • 自定义元类:基于元类自动为冻结数据类添加嵌套更新能力,适合需要批量处理多个数据类的场景。
  • 使用扩展库:比如attrs(比标准库dataclasses更灵活),结合自定义转换器实现自动嵌套处理。

装饰器实现示例:

from dataclasses import dataclass, replace, is_dataclass

def nested_immutable(cls):
    # 注入__post_init__:处理字典转子数据类
    original_post_init = getattr(cls, '__post_init__', lambda self: None)
    def __post_init__(self):
        original_post_init(self)
        for field_name, field_type in self.__annotations__.items():
            value = getattr(self, field_name)
            if is_dataclass(field_type) and isinstance(value, dict):
                object.__setattr__(self, field_name, field_type(**value))
    cls.__post_init__ = __post_init__

    # 注入__call__:处理嵌套更新
    def __call__(self, **kwargs):
        for key, value in kwargs.items():
            if key in self.__annotations__:
                field_type = self.__annotations__[key]
                if is_dataclass(field_type) and isinstance(value, dict):
                    original = getattr(self, key)
                    kwargs[key] = original(**value)
        return replace(self, **kwargs)
    cls.__call__ = __call__
    return cls

# 使用示例
@nested_immutable
@dataclass(frozen=True)
class Occupation:
    title: str
    sallary: int

@nested_immutable
@dataclass(frozen=True)
class Person:
    name: str
    occupation: Occupation

# 测试
a0 = Person(name='Jhon', occupation={'title': 'junear', sallary:30})
a1 = a0(name='Jhon Smith')
a2 = a1(occupation={'title': 'senior'})
a3 = a2(occupation={'sallary':50})
print(a3)  # 输出:Person(name='Jhon Smith', occupation=Occupation(title='senior', sallary=50))

内容的提问来源于stack exchange,提问作者forifone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 19:34:53