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)
Although in Python >=3.10 the kw_only feature was introduced that essentially makes inheritance in dataclasses much more reliable, the above example still can be used as a way to make dataclasses inheritable that do not require the usage of @dataclass decorator. Show activity on this post.
DataClass in Python DataClasses are like normal classes in Python, but they have some basic functions like instantiation, comparing, and printing the classes already implemented. Parameters: init: If true __init__() method will be generated. repr: If true __repr__() method will be generated.
In Python, "frozen" means an object cannot be modified.
Dataclasses resemble a lot with NamedTuples however namedtuples are immutable whereas dataclasses aren't (unless the frozen parameter is set to True.)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With