修改数据类对象,以便仅覆盖指定的元素

Modifying a dataclass object such that only specified elements are overridden

提问人:Ein Google-Nutzer 提问时间:10/13/2023 最后编辑:Ein Google-Nutzer 更新时间:10/14/2023 访问量:95

问:

我想创建一个包含大量成员元素的数据类。此 dataclass 不应具有成员,以确保完整信息在对象中可用。AOptional

然后我想要一个“修改选项”,它的成员与 相同,但作为可选成员。A

在不需要将成员写成两个不同类的情况下,最好的方法是什么?

这是我的方法(工作示例):

from copy import deepcopy
from dataclasses import dataclass
from typing import Optional

@dataclass
class A:
    x: int
    y: int


@dataclass
class A_ModificationOptions:
    x: Optional[int] = None
    y: Optional[int] = None


def modifyA(original: A, modification: A_ModificationOptions):
    if modification.x is not None:
        original.x = deepcopy(modification.x)

    if modification.y is not None:
        original.y = deepcopy(modification.y)


original_A = A(x=1, y=2)
print("A before modification: ", original_A) # A(x=1, y=2)

modification_A = A_ModificationOptions(y=7)
modifyA(original_A, modification_A)
print("A after modification: ", original_A) # A(x=1, y=7)

此代码满足以下要求:

  1. 原始成员没有可选成员,因此必须已设置所有成员。A
  2. 在修改中,只需要设置需要调整的成员。A

此代码不满足以下要求:

  1. 我不想再次“复制”每个成员。AA_ModificationOptions
  2. 如果可能的话,我不想拥有该功能,而是内置的东西。modifyA()
  3. 如果 2 是不可能的:我不想为 into 的每个成员添加 2 行。AmodifyA

有没有一种巧妙的方法来存储稀疏的“修改选项”,用于潜在的巨大数据类?

用例:用户创建一次完整列表,然后在不同的场景中,他可以玩弄该完整列表的增量,并且必须以某种方式存储完整列表的“增量”-> 所以我想到了一个原始的完整列表类和一个“增量”类,但我希望这在某种程度上可以以更简洁的方式完成。也许是智能深度复制之类的东西?AA_ModificationOptions

更新1:

谢谢@wjandrea的反馈!您对第 3 点的解决方案没有考虑更深的嵌套数据类,因此我使用您的建议使其适用于嵌套数据类。下面的代码现在解决了第 3 点:

from copy import deepcopy
from dataclasses import dataclass, is_dataclass
from typing import Optional


class Original:
    pass


@dataclass
class B(Original):
    a1: int
    a2: int
    a3: int


@dataclass
class A(Original):
    x: int
    y: int
    b: B


class Modification:
    pass


@dataclass
class B_Mod(Modification):
    a1: Optional[int] = None
    a2: Optional[int] = None
    a3: Optional[int] = None


@dataclass
class A_Mod(Modification):
    x: Optional[int] = None
    y: Optional[int] = None
    b: Optional[B_Mod] = None


def modifyDataclass(original: Original, modification: Modification):
    assert is_dataclass(original) and is_dataclass(modification)

    for k, v in vars(modification).items():
        if is_dataclass(v):
            assert isinstance(v, Modification)

            modifyDataclass(original=getattr(original, k), modification=v)

            return

        if v is not None:
            setattr(original, k, v)


original_A = A(x=1, y=2, b=B(a1=3, a2=4, a3=5))
print(
    "A before modification: ", original_A
)  # A(x=1, y=2, b=B(a1=3, a2=4, a3=5))

modification_A = A_Mod(y=7, b=B_Mod(a2=19))
modifyDataclass(original_A, modification_A)
print(
    "A after modification: ", original_A
)  # A(x=1, y=7, b=B(a1=3, a2=19, a3=5))

现在,如果第 1 点和第 2 点有解决方案,那就太棒了!

也许也以某种方式衍生?就像A_Mod是 A 的孩子,但后来将所有成员都切换为可选成员?

python python-typing deep-copy python-dataclasses

评论

0赞 wjandrea 10/13/2023
撇开重点不谈,但你为什么要深度复制 ints?是不是因为你没有显示的其他属性是容器,而你对所有属性都使用相同的代码?
0赞 wjandrea 10/13/2023
对于第 3 点,您可以使用 和 :vars()setattr()for k, v in vars(modification).items(): if v is not None: setattr(original, k, v)
0赞 Ein Google-Nutzer 10/13/2023
这只是一个简化的例子,真正的“原始”数据包含许多子数据类。感谢您对 3 的提示!
0赞 Silvio Mayolo 10/14/2023
这是通过静态类型检查器运行的,还是注释只是一种形式?我绝对可以写出一大块代码来轻松完成所有这些工作,但我想象的解决方案肯定不会与静态类型检查兼容。typingdataclass
0赞 Ein Google-Nutzer 10/14/2023
@SilvioMayolo 是的,我添加了 Original 和 Modification 类只是为了更好地进行类型检查,这样 original 和 modification 就不会混淆。对于解决方案,类型检查并不那么重要,我可以考虑添加它或不在此处使用它。你的建议是什么?:)

答:

0赞 Silvio Mayolo 10/14/2023 #1

我想我明白你想要什么,这里有一种动态生成类的方法。正如注释中所指出的,这永远不会通过静态类型检查器。如果你想运行类似或的东西,你将不得不退出修改选项。这是 Python 中非常动态的反映。A_ModificationOptionsmypypyrightAny

现在,几点说明。 是一个装饰器,就像任何装饰器一样,它只是在事后应用于类。那是dataclass

@dataclass
class X:
    ...

只是

class X:
    ...
X = dataclass(X)

因此,如果我们愿意,我们可以像普通的 Python 函数一样在我们组成的类上调用。当我们讨论这个话题时,我们也可以使用普通的 Python 制作类。type 有一个三参数形式,它充当新类的构造函数。dataclass

 class type(name, bases, dict, **kwds)

因此,让我们看看我们实际上是如何做到这一点的。我们需要和.我还导入以获得技术上正确的注释,尽管它不影响语义。dataclassfieldsOptional

from dataclasses import dataclass, fields
from typing import Optional

现在魔术酱,为方便起见评论。

def make_modification_dataclass(original_dataclass, new_class_name=None):
    # Provide a default name if the caller doesn't supply a custom
    # name for the new class.
    if new_class_name is None:
        new_class_name = original_dataclass.__name__ + "_ModificationOptions"
    # This actually creates the class. @dataclass is going to look at
    # the __annotations__ field on the class, which is normally
    # generated by writing type annotations in Python code. But it's
    # explicitly defined to be a mutable dictionary, so we're well
    # within our rights to create and mutate it ourselves.
    new_class = type(new_class_name, original_dataclass.__bases__, {
        "__annotations__": {}
    })
    # Iterate over all of the fields of the original dataclass.
    for field in fields(original_dataclass):
        # For each field, put a type in __annotations__. The type
        # could be anything as far as @dataclass is concerned, but we
        # make it Optional[whatever], which is actually the correct
        # type. No static type checker will ever see this, but other
        # tools that analyze __annotations__ at runtime will see a
        # correct type annotation.
        new_class.__annotations__[field.name] = Optional[field.type]
        # We also need to set the attribute itself on the class. This
        # is the "= None" part of the A_ModificationOptions class you
        # wrote, and it will show @dataclass what the default value of
        # the field should be.
        setattr(new_class, field.name, None)
    # Apply the decorator and return our brand new class.
    return dataclass(new_class)

要使用它,我们只需传递原始类并将结果分配给一个名称。

@dataclass
class A:
    x: int
    y: int

# This is making a class. A real, genuine dataclass.
A_ModificationOptions = make_modification_dataclass(A)

您的函数有点接近 dataclasses.replace,但后者 (a) 接受字典,(b) 返回一个新实例而不是就地更改。幸运的是,编写我们自己的代码相当简单。modifyA

这基本上是 wjandrea 在评论中建议的。我只是更喜欢使用而不是,因为它可以保证获取数据类字段,而不是从非数据类超类或四处闲逛并做有趣业务的人那里获得任何额外的内容。dataclasses.fieldsvars

def modify(original, modification):
    for field in fields(modification):
        value = getattr(modification, field.name)
        if value is not None:
            setattr(original, field.name, value)

您的代码将按建议工作。

original_A = A(x=1, y=2)
print("A before modification: ", original_A) # A(x=1, y=2)

modification_A = A_ModificationOptions(y=7)
modify(original_A, modification_A)
print("A after modification: ", original_A) # A(x=1, y=7)

我重命名了该函数,因为它实际上从未执行任何特定于 .这个函数将适用于任何和相应的类。无需重写它,即使是表面上的。modifymodifyAA@dataclass_ModificationOptions

在线试用!

评论

0赞 Ein Google-Nutzer 10/14/2023
非常感谢你,西尔维奥!这看起来太棒了,也感谢您的精心评论和解释:)。