Typage de canard, validation des données et programmation affirmée en Python

10

À propos de la frappe de canard :

Le typage de canard est aidé en ne testant généralement pas le type d'arguments dans les corps de méthode et de fonction, en s'appuyant sur la documentation, un code clair et des tests pour garantir une utilisation correcte.

À propos de la validation des arguments (EAFP: plus facile de demander pardon que permission). Un exemple adapté d' ici :

... il est considéré comme plus pythonique de faire:

def my_method(self, key):
    try:
        value = self.a_dict[member]
    except TypeError:
        # do something else

Cela signifie que toute autre personne utilisant votre code n'a pas besoin d'utiliser un véritable dictionnaire ou une sous-classe - ils peuvent utiliser n'importe quel objet qui implémente l'interface de mappage.

Malheureusement, dans la pratique, ce n'est pas si simple. Que faire si le membre dans l'exemple ci-dessus peut être un entier? Les entiers sont immuables - il est donc parfaitement raisonnable de les utiliser comme clés de dictionnaire. Cependant, ils sont également utilisés pour indexer des objets de type séquence. Si le membre se trouve être un entier, l'exemple deux pourrait laisser passer des listes et des chaînes ainsi que des dictionnaires.

À propos de la programmation affirmée :

Les assertions sont un moyen systématique de vérifier que l'état interne d'un programme est celui attendu par le programmeur, dans le but de détecter les bogues. En particulier, ils sont bons pour détecter les fausses hypothèses qui ont été faites lors de l'écriture du code, ou pour abuser d'une interface par un autre programmeur. De plus, ils peuvent agir en tant que documentation en ligne dans une certaine mesure, en rendant évidentes les hypothèses du programmeur. ("Explicite vaut mieux qu'implicite.")

Les concepts mentionnés sont parfois en conflit, donc je compte sur les facteurs suivants pour choisir si je ne fais aucune validation des données, si je fais une validation forte ou si j'utilise des assertions:

  1. Validation forte. Par validation forte, j'entends lever une exception personnalisée ( ApiErrorpar exemple). Si ma fonction / méthode fait partie d'une API publique, il est préférable de valider l'argument pour afficher un bon message d'erreur sur un type inattendu. En vérifiant le type, je ne veux pas dire utiliser uniquement isinstance, mais aussi si l'objet passé prend en charge l'interface nécessaire (typage canard). Alors que je documente l'API et spécifie le type attendu et que l'utilisateur peut vouloir utiliser ma fonction de manière inattendue, je me sens plus en sécurité lorsque je vérifie les hypothèses. J'utilise habituellement isinstanceet si plus tard je veux supporter d'autres types ou canards, je change la logique de validation.

  2. Programmation affirmée. Si mon code est nouveau, j'utilise beaucoup les affirmations. Quels sont vos conseils à ce sujet? Supprimez-vous plus tard les assertions du code?

  3. Si ma fonction / méthode ne fait pas partie d'une API, mais transmet certains de ses arguments à un autre code non écrit, étudié ou testé par moi, je fais beaucoup d'assertions en fonction de l'interface appelée. Ma logique derrière cela - mieux vaut échouer dans mon code, puis quelque part 10 niveaux plus profonds dans stacktrace avec une erreur incompréhensible qui force à déboguer beaucoup et à ajouter plus tard l'assertion à mon code de toute façon.

Commentaires et conseils sur le moment d'utiliser ou de ne pas utiliser la validation de type / valeur, affirme-t-il? Désolé pour la meilleure formulation de la question.

Par exemple, considérons la fonction suivante, où se Customertrouve un modèle déclaratif SQLAlchemy:

def add_customer(self, customer):
    """Save new customer into the database.
    @param customer: Customer instance, whose id is None
    @return: merged into global session customer
    """
    # no validation here at all
    # let's hope SQLAlchemy session will break if `customer` is not a model instance
    customer = self.session.add(customer)
    self.session.commit()
    return customer

Il existe donc plusieurs façons de gérer la validation:

def add_customer(self, customer):
    # this is an API method, so let's validate the input
    if not isinstance(customer, Customer):
        raise ApiError('Invalid type')
    if customer.id is not None:
        raise ApiError('id should be None')

    customer = self.session.add(customer)
    self.session.commit()
    return customer

ou

def add_customer(self, customer):
    # this is an internal method, but i want to be sure
    # that it's a customer model instance
    assert isinstance(customer, Customer), 'Achtung!'
    assert customer.id is None

    customer = self.session.add(customer)
    self.session.commit()
    return customer

Quand et pourquoi utiliseriez-vous chacun de ces éléments dans le contexte de la frappe de canard, de la vérification de type et de la validation des données?

warvariuc
la source
1
vous ne devez pas supprimer les assertions comme les tests unitaires, sauf pour des raisons de performances
Bryan Chen

Réponses:

4

Permettez-moi de vous donner quelques principes directeurs.

Principe # 1. Comme indiqué dans http://docs.python.org/2/reference/simple_stmts.html, la surcharge de performances des assertions peut être supprimée avec une option de ligne de commande, tout en étant là pour le débogage. Si les performances sont un problème, faites-le. Laissez les assertions. (Mais ne faites rien d'important dans les assertions!)

Principe # 2. Si vous affirmez quelque chose et que vous avez une erreur fatale, utilisez une assertion. Il n'y a absolument aucune valeur à faire autre chose. Si quelqu'un veut changer cela plus tard, il peut changer votre code ou éviter cet appel de méthode.

Principe # 3. Ne refusez pas quelque chose simplement parce que vous pensez que c'est une chose stupide à faire. Et si votre méthode autorise les chaînes? Si ça marche, ça marche.

Principe # 4. Interdisez les choses qui sont des signes d'erreurs probables. Par exemple, envisagez de passer un dictionnaire d'options. Si ce dictionnaire contient des éléments qui ne sont pas des options valides, cela signifie que quelqu'un n'a pas compris votre API ou qu'il avait une faute de frappe. Faire sauter cela est plus susceptible d'attraper une faute de frappe que d'empêcher quelqu'un de faire quelque chose de raisonnable.

Sur la base des 2 premiers principes, votre deuxième version peut être jetée. Lequel des deux autres vous préférez est une question de goût. Lequel pensez-vous le plus probable? Que quelqu'un passera un non-client à add_customeret les choses vont se casser (dans ce cas, la version 3 est préférée), ou que quelqu'un voudra à un moment donné remplacer votre client par un objet proxy d'une sorte qui répond à toutes les bonnes méthodes (auquel cas la version 1 est préférée).

Personnellement, j'ai vu les deux modes de défaillance. J'aurais tendance à opter pour la version 1 en partant du principe général que je suis paresseux et qu'il faut moins taper. (De plus, ce type d'échec a tendance à apparaître tôt ou tard d'une manière assez évidente. Et quand je veux utiliser un objet proxy, je suis vraiment ennuyé par les gens qui m'ont attaché les mains.) Mais il y a des programmeurs que je respecte qui irait dans l'autre sens.

btilly
la source
Je préfère la v.3, en particulier lors de la conception de l'interface - écriture de nouvelles classes et méthodes. Je considère également que la v.3 est utile pour les méthodes API - car mon code est nouveau pour les autres. Je pense que l'approche affirmée est un bon compromis, car elle est supprimée en production lors de l'exécution en mode optimisé. > Faire exploser cela est plus susceptible d'attraper une faute de frappe que d'empêcher quelqu'un de faire quelque chose de raisonnable. <Alors, ça ne vous dérange pas d'avoir une telle validation?
warvariuc
Disons-le de cette façon. Je trouve que l'héritage correspond mal à la façon dont j'aime faire évoluer les conceptions. Je préfère la composition. Je répugne donc à affirmer que cela doit être de cette classe. Mais je ne suis pas opposé aux affirmations où je pense qu'ils me sauvent quelque chose.
btilly