扩展冻结数据类并从基类实例中获取所有数据

far*_*ven 7 python python-3.x python-dataclasses

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

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

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

pluto = dog_factory() # returns a Dog object
Run Code Online (Sandbox Code Playgroud)

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

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

class AngryDog(Dog):
    # what will come here ?
Run Code Online (Sandbox Code Playgroud)

有没有办法避免手动将所有类 Dog 参数放入init中?像复制构造函数之类的东西。

理想情况下:

class AngryDog(Dog):
    def __init__(self, dog, bite = True):
        copy_construct(dog)
Run Code Online (Sandbox Code Playgroud)

Arn*_*rne 6

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

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

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)
Run Code Online (Sandbox Code Playgroud)

但遵循这种模式,您将面临一个特定的边缘情况,您自己已经通过参数指出了这whatever一点。重新调用Dog构造函数时,调用InitVar中将缺少任何asdict内容,因为它们不是该类的正确成员。事实上,数据类中发生的任何事情都__post_init__可能InitVars导致错误或意外行为。

如果只是一些小事情,例如从调用中过滤或删除已知参数,cls并且父类预计不会更改,那么您可以尝试在from_dog. 但从概念上讲,没有办法为此类from_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)
Run Code Online (Sandbox Code Playgroud)

用法:

# 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
Run Code Online (Sandbox Code Playgroud)

但最终,重点仍然是isinstance(dog_e, Dog)回归False。如果您决心让该调用 return 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)
Run Code Online (Sandbox Code Playgroud)

用法:

# 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
Run Code Online (Sandbox Code Playgroud)

大多数添加数据类的方法,例如__repr__,都会被破坏,包括将分身实例插入类似dataclass.asdict或什至只是这样的东西中vars- 因此使用时请自行承担风险。