【问题标题】:Extending frozen dataclass and take all data from base class instance扩展冻结数据类并从基类实例中获取所有数据
【发布时间】:2020-09-30 23:49:30
【问题描述】:

假设我们有一个来自图书馆的类,

@dataclass(frozen=True)
class Dog:
    name: str
    blabla : int
    # lot of parameters
    # ...
    whatever: InitVar[Sequence[str]]

我有一个来自外部库的狗构造函数。

pluto = dog_factory() # returns a Dog object

我希望这只狗有一个新成员,比如说“bite”。 显然pluto['bite'] = True 会失败,因为数据类被冻结了。

所以我的想法是从 Dog 创建一个子类并从 'pluto' 实例中获取所有数据。

class AngryDog(Dog):
    # what will come here ?

有没有办法避免手动将所有类 Dog 参数放入 init ? 类似于复制构造函数。

理想情况下:

class AngryDog(Dog):
    def __init__(self, dog, bite = True):
        copy_construct(dog)

【问题讨论】:

    标签: python python-3.x python-dataclasses


    【解决方案1】:

    如果你想使用继承来解决你的问题,你需要从编写一个合适的AngryDog 子类开始,你可以使用它来构建健全的实例。

    下一步是添加一个from_dogclassmethod,可能是这样的:

    from dataclasses import dataclass, asdict
    
    @dataclass(frozen=True)
    class AngryDog(Dog):
        bite: bool = True
    
        @classmethod
        def from_dog(cls, dog: Dog, **kwargs):
            return cls(**asdict(dog), **kwargs)
    

    但是按照这种模式,您将面临一个特定的极端情况,您自己已经通过whatever 参数指出了这种情况。当重新调用Dog 构造函数时,任何InitVar 都将在asdict 调用中丢失,因为它们不是该类的正确成员。事实上,任何发生在数据类的__post_init__(InitVars 所在的位置)中的任何事情都可能导致错误或意外行为。

    如果只是像过滤或删除 cls 调用中的已知参数这样的小事情,并且预计父类不会更改,您可以尝试在 from_dog 中处理它。但从概念上讲,没有办法为这种from_instance 问题提供通用解决方案。


    Composition 从数据完整性的角度来看是没有错误的,但考虑到手头的确切问题,它可能是单一的或笨拙的。这样的 dog-extension 不能代替适当的 dog-instance 使用,但我们可以将其鸭型化成正确的形状以备不时之需:

    class AngryDogExtension:
        def __init__(self, dog, bite=True):
            self.dog = dog
            self.bite = bite
    
        def __getattr__(self, item):
            """Will make instances of this class bark like a dog."""
            return getattr(self.dog, item)
    

    用法:

    # starting with a basic dog instance
    >>> dog = Dog(name='pluto', blabla=1, whatever=['a', 'b'])
    
    >>> dog_e = AngryDogExtension(d)
    >>> dog_e.bite  # no surprise here, just a regular member
    True
    >>> dog_e.name  # this class proxies its dog member, so no need to run `dog_e.dog.name` 
    pluto
    

    但归根结底,isinstance(dog_e, Dog) 将返回 False。如果您承诺让该调用返回True,那么有一些高级技巧可以帮助您,并使任何继承您代码的人讨厌您:

    class AngryDogDoppelganger(Dog):
        def __init__(self, bite, **kwargs):
            if "__dog" in kwargs:
                object.__setattr__(self, "__dog", kwargs["__dog"])
            else:
                object.__setattr__(self, "__dog", Dog(**kwargs))
            object.__setattr__(self, "bite", bite)
    
        @classmethod
        def from_dog(cls, dog, bite=True):
            return cls(bite, __dog=dog)
    
        def __getattribute__(self, name):
            """Will make instances of this class bark like a dog.
    
            Can't use __getattr__, since it will see its own instance
            attributes. To have __dog work as a proxy, it needs to be
            checked before basic attribute lookup. 
            """
            try:
                return getattr(object.__getattribute__(self, "__dog"), name)
            except AttributeError:
                pass
            return object.__getattribute__(self, name)
    

    用法:

    # starting with a basic dog instance
    >>> dog = Dog(name='pluto', blabla=1, whatever=['a', 'b'])
    
    # the doppelganger offers a from_instance method, as well as 
    # a constructor that works as expected of a subclass
    >>> angry_1 = AngryDogDoppelganger.from_dog(dog)
    >>> angry_2 = AngryDogDoppelganger(name='pluto', blabla=1, whatever=['a', 'b'], bite=True)
    
    # instances also bark like at dog, and now even think they're a dog
    >>> angry_1.bite  # from subclass
    True
    >>> angry_1.name  # looks like inherited from parent class, is actually proxied from __dog
    pluto
    >>> isinstance(angry_1, Dog)  # ?
    True
    

    大多数添加数据类的方法,例如 __repr__,将被破坏,包括在 dataclass.asdict 甚至只是 vars 之类的东西中插入分身实例 - 所以使用风险自负。

    【讨论】:

    • 感谢您的回答!我知道这会起作用,我的问题是我有一个构建狗对象的库,我想从该库中生成一个 AngryDoc,而不必一一手动构造所有属性。
    • 啊,我明白了。我将尝试扩展答案。
    • 您可能已经知道,因为您明确添加了它,但 InitVar 使子类化变得棘手/不可能。当您从旧实例构建新实例时,它不再存在。你考虑过composition吗?
    • 是的,我尝试过这样做,并以这种方式获得了“最佳”结果,但是当我需要它时,我仍然需要解压缩“Dog”实例。你有什么更好的想法吗?
    • 我不会说“更好”,但我正在重写我的答案以提供一些背景信息。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多