7
votes

Validation de l'entrée lors de la mutation d'une classe de données

Dans Python 3.7, il y a ces nouveaux conteneurs "dataclass" qui sont fondamentalement comme des tuples nommés mutables. Supposons que je crée une classe de données destinée à représenter une personne. Je peux ajouter une validation d'entrée via la fonction __post_init __ () comme ceci:

someone = Person(name="John Doe", age=30)
someone.age = -30
print(someone)

Person(name='John Doe', age=-30)

Cela permettra de bonnes entrées via:

someone = Person(name=["John Doe"], age=30)
someone = Person(name="John Doe", age="thirty")
someone = Person(name="John Doe", age=-30)


3 commentaires

Utilisez @dataclass (Frozen = True) pour le rendre "immuable"


@ juanpa.arrivillaga qui irait à l'encontre de l'objectif d'utiliser une classe de données en premier lieu. Si je voulais un conteneur de données immuable, j'utiliserais simplement un namedtuple. J'ai l'intention de pouvoir mettre à jour les champs quelque temps après avoir initialisé la variable.


Eh bien, les namedtuples sont des tuples , un @dataclass est juste un décorateur qui vous permet d'éviter d'écrire beaucoup de passe-partout pour créer des classes d'un type fréquemment rencontré, ce n'est pas seulement "un nom de nom mutable". Mais alors je suppose, vous devrez cacher vos attributs derrière une propriété ou quelque chose, cependant, cela supprimerait une partie de la gentillesse de la classe de données pour commencer


3 Réponses :


1
votes

Peut-être verrouiller l'attribut en utilisant getters et setters au lieu de muter l'attribut directement . Si vous extrayez ensuite votre logique de validation dans une méthode distincte, vous pouvez valider de la même manière à la fois à partir de votre setter et de la fonction __post_init__ .


0 commentaires

12
votes

Les classes de données sont un mécanisme permettant de fournir une initialisation par défaut pour accepter les attributs en tant que paramètres, et une belle représentation, ainsi que quelques subtilités comme le hook __post_init__ .

Heureusement, elles ne dérangent aucun autre mécanisme d'accès aux attributs en Python - et vous pouvez toujours créer vos attributs sans classe de données en tant que descripteurs de propriété , ou en tant que classe de descripteur personnalisée si vous le souhaitez. De cette façon, tout accès aux attributs passera automatiquement par vos fonctions getter et setter.

Le seul inconvénient de l'utilisation de la propriété intégrée par défaut est que vous devez l'utiliser dans «l'ancienne méthode», et non avec la syntaxe du décorateur - qui vous permet de créer des annotations pour vos attributs.

Ainsi, les «descripteurs» sont des objets spéciaux affectés aux attributs de classe en Python d'une manière que tout accès à cet attribut appellera les méthodes des descripteurs __get__ , __set__ ou __del__ . La propriété intégrée est un moyen de construire un descripteur passé 1 à 3 fonctions qui seront appelées à partir de ces méthodes.

Donc, sans descripteur personnalisé, vous pouvez faire:

def positive_validator(name, value):
    if value <= 0:
        raise ValueError(f"values for {name!r}  have to be positive")

class MyAttr:
     def __init__(self, type, validators=()):
          self.type = type
          self.validators = validators

     def __set_name__(self, owner, name):
          self.name = name

     def __get__(self, instance, owner):
          if not instance: return self
          return instance.__dict__[self.name]

     def __delete__(self, instance):
          del instance.__dict__[self.name]

     def __set__(self, instance, value):
          if not isinstance(value, self.type):
                raise TypeError(f"{self.name!r} values must be of type {self.type!r}")
          for validator in self.validators:
               validator(self.name, value)
          instance.__dict__[self.name] = value

#And now

@dataclass
class Person:
    name: str = MyAttr(str)
    age: float = MyAttr((int, float), [positive_validator,])

En utilisant cette approche, vous devrez écrire l'accès de chaque attribut comme deux méthodes / fonctions, mais vous n'aurez plus besoin d'écrire votre code __post_init __ >: chaque attribut se validera.

Notez également que cet exemple a pris la petite approche habituelle de stockage des attributs normalement dans le __dict__ de l'instance. Dans les exemples sur le Web, la pratique consiste à utiliser un accès aux attributs normal, mais en ajoutant au nom un _ . Cela laissera ces attributs polluer un dir sur votre instance finale, et les attributs privés ne seront pas protégés.

Une autre approche consiste à écrire votre propre classe de descripteur et à la laisser vérifier l'instance et les autres propriétés des attributs que vous souhaitez protéger. Cela peut être aussi sophistiqué que vous le souhaitez, aboutissant à votre propre cadre. Donc, pour une classe de descripteur qui vérifiera le type d'attribut et acceptera une liste de validateurs, vous aurez besoin de:

@dataclass
class MyClass:
   def setname(self, value):
       if not isinstance(value, str):
           raise TypeError(...)
       self.__dict__["name"] = value
   def getname(self):
       return self.__dict__.get("name")
   name: str = property(getname, setname)
   # optionally, you can delete the getter and setter from the class body:
   del setname, getname

Voilà - la création de votre propre classe de descripteur nécessite un peu plus connaissances sur Python, mais le code donné ci-dessus devrait être bon pour une utilisation, même en production - vous êtes invités à l'utiliser.

Notez que vous pouvez facilement ajouter beaucoup d'autres vérifications et transformations pour chacun de vos attributs - et le code dans __set_name__ lui-même pourrait être modifié pour introspecter les __annotations__ dans la classe owner pour prendre automatiquement note des types - afin que le type ne serait pas nécessaire pour la classe MyAttr elle-même. Mais comme je l'ai déjà dit: vous pouvez le rendre aussi sophistiqué que vous le souhaitez.


5 commentaires

Notez que l'utilisation de property le fait se comporter comme un champ avec une valeur par défaut, c'est-à-dire qu'il ne peut pas être utilisé avant un champ sans valeur par défaut ("TypeError: argument non par défaut 'second' suit l'argument par défaut). I ' J'ai fini par utiliser __setattr__ sur la classe de données encapsulée pour invalider une partie du cache si un champ / attribut est défini.


Oui - les classes de données prendront n'importe quel champ avec un descripteur comme ayant une "valeur par défaut" - la seule façon de changer cela serait d'attribuer les descripteurs après que le décorateur @dataclass a été exécuté - cela nécessiterait à la fois un autre décorateur et un moyen d'annoter les descripteurs eux-mêmes.


@jsbueno, je commence juste à donner un sens aux classes Python, mais ai-je raison de dire que dans votre premier exemple, "name" devrait être cité dans self .__ dict __. get (name) ?


Oui, cela aurait dû être cité. Je suis en train de réparer maintenant.


Au lieu de créer explicitement votre propre classe de descripteur, il peut être plus simple, selon ce que vous faites, d'utiliser simplement la fonction intégrée property () pour en créer une - similaire au typed_property () fonction montrée dans cette réponse de la mienne.



0
votes

Une solution simple et flexible peut être de remplacer la méthode __setattr__ :

@dataclass
class Person:
    name: str
    age: float

    def __setattr__(self, name, value):
        if name == 'age':
            assert value > 0, f"value of {name} can't be negative: {value}"
        self.__dict__[name] = value


2 commentaires

Bien que cela fonctionne, ce n'est pas évolutif. Mieux vaut utiliser quelque chose d'un peu plus méta / introspectif.


@Rebs Pourquoi n'est-il pas évolutif?