4 votes

Extension d'une classe de données gelée et reprise de toutes les données de l'instance de la classe de base

Supposons qu'une classe vienne d'une bibliothèque,

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

J'ai un constructeur de chien qui provient d'une bibliothèque externe.

pluto = dog_factory() # returns a Dog object

J'aimerais que ce chien ait un nouveau membre, disons ''. bite '. Il est évident que pluto['bite'] = True échouera, car la classe de données est gelée.

Mon idée est donc de créer une sous-classe de Dog et de récupérer toutes les données de l'instance 'pluto'.

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

Existe-t-il un moyen d'éviter de placer manuellement tous les paramètres de la classe Dog dans init ? Quelque chose comme un constructeur de copie.

idéalement :

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

4voto

Arne Points 4882

Si vous souhaitez utiliser l'héritage pour résoudre votre problème, vous devez commencer par écrire un fichier de type AngryDog que vous pouvez utiliser pour construire des instances saines.

L'étape suivante consisterait à ajouter un from_dog méthode de classe Il s'agit peut-être de quelque chose comme cela :

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)

Mais en suivant ce modèle, vous serez confronté à un cas particulier, que vous avez vous-même déjà signalé par l'intermédiaire de l'option whatever paramètre. Lorsque l'on rappelle le Dog tout constructeur de InitVar manquera dans un asdict puisqu'il n'est pas un membre à part entière de la classe. En fait, tout ce qui se passe dans la classe de données d'une __post_init__ , qui est l'endroit où InitVars peut entraîner des bogues ou un comportement inattendu.

S'il ne s'agit que de choses mineures, comme le filtrage ou la suppression de paramètres connus du cls et qu'il n'est pas prévu que la classe mère change, vous pouvez simplement essayer de le gérer en from_dog . Mais il n'existe conceptuellement aucun moyen de fournir une solution générale pour ce type de problème. from_instance problème.


Composition fonctionnerait sans problème du point de vue de l'intégrité des données, mais pourrait être peu idiomatique ou maladroit compte tenu de l'objet exact de l'opération. Une telle extension de chien ne serait pas utilisable à la place d'une véritable instance de chien, mais nous pourrions lui donner la bonne forme au cas où cela serait nécessaire :

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)

Utilisation :

# 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

Mais en fin de compte, le fait est que isinstance(dog_e, Dog) renverra False . Si vous vous engagez à faire ce retour d'appel True Il existe des astuces avancées pour vous aider et faire en sorte que tous ceux qui héritent de votre code vous détestent :

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)

Utilisation :

# 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

La plupart des méthodes d'ajout de données, comme le __repr__ Cependant, le système ne fonctionnera pas, notamment en ce qui concerne l'insertion d'instances de doublons dans des éléments tels que dataclass.asdict ou même simplement vars - à utiliser à ses propres risques.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X