Suppose we have a class coming from a library,
@dataclass(frozen=True) class Dog: name: str blabla : int # lot of parameters # ... whatever: InitVar[Sequence[str]]
I have a dog constructor coming from an external library.
pluto = dog_factory() # returns a Dog object
I would like this dog to have a new member, let’s say ‘bite
‘.
Obviously pluto['bite'] = True
will fail, since dataclass is frozen.
So my idea is to make a subclass from Dog and get all the data from the ‘pluto’ instance.
class AngryDog(Dog): # what will come here ?
Is there a way to avoid manually put all the class Dog parameters in init ? Something like a copy constructor.
ideally:
class AngryDog(Dog): def __init__(self, dog, bite = True): copy_construct(dog)
Advertisement
Answer
If you want to use inheritance to solve your problem, you need to start off with writing a proper AngryDog
subclass that you can use to build sane instances from.
The next step would be to add a from_dog
classmethod, something like this maybe:
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)
But following this pattern, you’ll face a specific edge case, which you yourself already pointed out through the whatever
parameter. When re-calling the Dog
constructor, any InitVar
will be missing in an asdict
call, since they are not a proper member of the class. In fact, anything that takes place in a dataclass’ __post_init__
, which is where InitVars
go, might lead to bugs or unexpected behavior.
If it’s only minor stuff like filtering or deleting known parameters from the cls
call and the parent class is not expected to change, you can just try to handle it in from_dog
. But there is conceptually no way to provide a general solution for this kind of from_instance
problem.
Composition would work bug-free from a data-integrity perspective, but might be unidiomatic or clunky given the exact matter at hand. Such a dog-extension wouldn’t be usable in-place of a proper dog-instance, but we could duck-type it into the right shape in case it’s necessary:
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)
Usage:
# 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
But ultimately, the point remains that isinstance(dog_e, Dog)
will return False
. If you’re committed to make that call return True
, there is some advanced trickery to help you out, and make anyone who inherits your code hate you:
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)
Usage:
# 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
Most of the dataclass-added methods, like __repr__
, will be broken though, including plugging doppelganger instances in things like dataclass.asdict
or even just vars
– so use at own risk.