Les contrats en pratique - Dessiner un SVG
Suite de ma série de billets de blog. Cette fois-ci j'utilise les contrats pour être productif rapidement et appréhender facilement une librairie complexe 🏎️

Ce post est la suite de Compter les moutons avec des contrats en Python
Les contrats sont une technique de code, qui nécessite de la pratique pour être bien compris. Dans ce post, je vais détailler des méthodes permettant une mise en place rapide et maintenables à long terme.
Nous allons faire quelque chose de nouveau: nous allons dessiner avec notre code. Je vais utiliser la librairie Pycairo pour générer un SVG. Ce post est une excuse pour aborder la mise en pratique des contrats concrètement tout en s'amusant. Pour une difficulté supplémentaire, je ne connais pas Pycairo donc je vais me servir des contrats pour faciliter ma découverte de l'API et sa prise en main.
Méthodologie
J'utilise un prototype sans aucun code de test: c'est le code lui-même qui sera mon test: si il tourne alors il marche. On appelle souvent cela un smoke test ("Si ça fait de la fumée alors ça tourne") et ils ont assez mauvaise réputation puisqu'ils se contentent souvent de regarder la sortie, sans tester le code lui même. En utilisant des contrats, j'ajoute cependant beaucoup d'assertions, comme autant de mini tests, qui vont guider mon développement à coup de messages d'erreur. Il ne s'agit ni plus ni moins que d'une technique de travail sous contrainte.
Dans ce contexte, les contrats peuvent être compris comme des garde-corps: ils sont là pour m'orienter rapidement et efficacement vers le bon résultat.
Avant de démarrer, un point très haut niveau: la mise en place initiale des contrats a un coût en efforts et en temps. C'est un investissement, de la même manière que cela demande du temps de documenter le code, de le tester, ou de vérifier qu'un algorithme fonctionne correctement. Il s'agit donc de rendre cet investissement le plus rentable possible, en réduisant l'effort associé et en augmentant leur effet de levier. C'est pourquoi dans cet exemple je m'attache a utiliser des techniques qui fournissent un bon retour sur investissement, sans qu'ils soient exhaustifs (ce qui serait nécessaire pour une preuve formelle).
Le juste milieu que j'ai trouvé est assez qualitatif, après tout du moment ou on a une belle image on peut s'estimer content. Cependant dans un contexte professionnel, il n'est pas rare qu'un tel programme soit réemployé, alors j'ai considéré que cette approche de "prototype contractualisé" constitue ce juste milieu.
Architecture générale
Puisque Pycairo est basé sur un ensemble limité et documenté de classes, j'ai choisi une approche basée sur l'héritage de classe pour mon interface "bas-niveau".
La classe mère, que j'appelle aussi classe implémentation, est celle que me fournit Pycairo. La classe fille, que j'appelle classe spécification est celle que j'implémente: elle hérite de la classe mère et rajoute des méthodes avec des contrats.
L'ensemble des conditions est stockée dans des parties du code que je nomme spécification. On l'appelle souvent interface mais je trouve ce terme un peu vague dans ce contexte. Si on y regarde de plus près cette partie du code n'est autre qu'un formalisation exécutable de la spécification. J'emploierai donc ce terme de spécification pour en parler. Dans le cas de la spécification écrite s'il y en a une, je l'appelle souvent spécification papier pour la distinguer.
Par ailleurs quelques liens si il y a un peu de flou dans ces définitions de spécification exécutable, spécification papier, et cahier des charges.
Notez que la spécification est séparée de l'implémentation: il y a une séparation nette entre la partie contrat et la partie implémentation. On peut rapprocher cela de la notion d'interface en programmation orientée objets: les contrats décrivent le comportement attendu tandis que le code PyCairo décrit le comportement de l'implémentation.
On peut également faire un lien avec le concept d'encapsulation: les contrats font partie de l'interface externe, et ignorent tout des données internes.
Un paradoxe: La classe fille ajoute des conditions sur les entrées: elle rejette donc certaines entrées qu'acceptait la classe mère. Ceci contredit un principe important: le principe de substitution de Liskov (LSP).
Le principe spécifie que pour toute propriété positive du système parent, le système qui en hérite doit remplir la même propriété.
En langage commun: partout ou la classe parente peut être utilisée, je dois pouvoir utiliser la classe fille sans changer le code utilisateur - donc je dois pouvoir faire une substitution.
Par exemple: un chat est un animal, et un animal peut marcher, donc dans un code faisant marcher un animal je dois pouvoir le remplacer directement par un chat (héritant de la classe animal).
Maintenant pour la controverse: un chat qui dort ne peux pas marcher. Il ne s'agit donc plus d'un animal selon le LSP car il n'est plus substituable.
Dans notre cas c'est un faux paradoxe: les entrées qu'accepte la classe mère mais que rejette la classe fille ne respectent pas les contrats: elles sont donc invalides, et c'est un défaut de la classe mère de les avoir acceptées en premier lieu. En d'autres termes, la classe mère peut être substituée par la classe fille sans altérer ses propriétés désirables du système (les seules qui nous intéressent).
J'ai encapsulé une partie de Pycairo dans des objets Spec, ça peut être considérée peu pythonic car le Zen de Python propose de préférer le déroulé à l'imbriqué (principe 5). Le déroulé signifie ajouter du code avant ou après le code existant directement, tandis que l'imbriqué signifie concevoir un nouvel objet ou une nouvelle fonction pour le faire.
Dérouler les contrats directement dans le code de Pycairo serait un objectif louable mais:
- Il faudrait modifier le code de Pycairo
- Pycairo lèverait alors de nouvelles exception dans certains code utilisateur existant, et tout le monde sait que tout bug non identifié est une fonctionnalité qu'utilisent les clients de la librairie.
- Cela rajouterait de la complexité a Pycairo, et une dépendance au framework de DbC.
- Ce serait en temps et efforts pour un post de blog 😉

Je pense faire un billet sur le sujet, dites moi si ça vous intéresse !
Interface Pycairo
Les contrats peuvent s'appliquer a plusieurs niveaux: la structure des données (typage), le contenu du ces données, et les valeurs croisées des données entre elles (ainsi qu'avec l'environnement).
Schématiquement on pourrait dire que nos données et les méthodes pour les vérifier sont représentées par le schéma suivant (avec un exemple bidon):
Duck-typing
Le duck-typing est le type de typage que préfère python. Il s'agit de vérifier le comportement et les capacités du type, et non sa représentation.
Par exemple si j'utilise le parametre
X
uniquement pour appeler X.couac()
alors son duck-type est "tout objet qui implémente une méthode couac()
"Python a une famille de types: les types numériques, qui stockent des nombres. Ils étendent tous la classe numbers.Number
.
Il demande par ailleurs souvent des nombres entre 0 et 1, je peux le vérifier avec les conditions suivantes:
def is_numeric(v):
return isinstance(v, numbers.Number) and v != float('nan')
def is_unit(v):
return is_numeric(v) and 0.0 <= v and v <= 1.0
Je commence par vérifier que j'ai bien reçu un type numérique, et qu'il ne s'agit pas du flottant Not-a-number, puis je peux vérifier la valeur. Ces fonctions doivent dans tous les cas retourner une valeur booléenne, soit vrai si le paramètre correspond a la cible, soit faux si ce n'est pas le cas. Elles ne doivent jamais lever d'exception. C'est paradoxal car lorsqu'un contrat échoue il lève une exception. Je peux le justifier par le partage des responsabilités: la spécification vérifie la validité sans plus de détail, la partie contrats communique au niveau supérieur l'échec d'une condition avec une exception.
Dans mon interface de spécification, j'ai défini de nouvelles opérations qui prennent des structures de données (un poil) plus complexes que celles que manipule Pycairo. Ainsi pour une position X
, Y
je passe un tuple de deux éléments.
On aura l'appel suivant
def has_len(x):
return hasattr(x, "__len__")
def has_iter(x):
return hasattr(x, "__iter__")
def is_point(p):
return (has_len(p) and len(p) == 2
and has_iter(p) and all(is_numeric(c) for c in p))
On vérifie le duck-type explicitement avant d'utiliser la fonction len()
Puisque je dois vérifier que je reçois bien deux nombres, je vérifie d'abord la taille du contenant. Je ne vérifie pas son type directement car je respecte (un minimum) le duck-typing. Je vérifie donc que len() == 2
.
hasattr(x, "__len__")
Je vérifie ensuite que l'ensemble des objets présent dans ce contenant sont bien des type numériques.
__iter__
et l'indexation __getitem__
sont deux interfaces complémentaires pour manipuler un contenant python. J'utilise la première ici, mais la seconde serait valide. Notez que si je voulais un contrat béton j'utiliserai l'une ou l'autre en fonction de sa disponibilité.Derrière l'expertise nécessaire à l'implémentation des fonction de la spécification, il y a de grandes possibilités de factorisation du code. Un autre sujet à traiter...
Types machine
Puisque Pycairo est basé sur une libraire C (libcairo), les objets que nous lui fournissons doivent être du type machine correct: flottant, entier...
Deux éléments sont nécessaires (mais non suffisant) pour se représenter ces types: la taille du type (32, 64 bits...) et son encodage (entier signé ou non, flottant...). Dans notre cas c'est son encodage qui nous sera important: si je passe une valeur non encodable, l'interface python vers le C me lèvera une erreur.
A - Donnez une valeur de flottant 32 bits qui n'est pas encodable en entier signé 32 bits
Réponse:
1.0E10
qui est trop large et causerait un overflow.B - Donnez une valeur d'entier signé 32 bits qui n'est pas encodable en flottant 32 bits
Réponse:
0x0000_ffff_ffff_ffff
qui causerait une perte de précisionPar exemple, une taille width
, height
est passée dans Pycairo sous forme de deux entiers exclusivement: passer un flottant causera une erreur au niveau de l'interface avec le code C. Python manipule les données comme entiers si et seulement si elles n'ont pas de partie fractionnelle.
Vérifier qu'il s'agit d'un entier peut donc être fait de la manière suivante
def is_int(x):
if isinstance(x, int):
return True
elif isinstance(x, float):
return x.is_integer()
elif is_numeric(x):
return round(x) == x
else:
return False
Cet avantage nous est utile car les contrats servent alors de documentation très claire.
Plus tard, nous pourrons contourner les deux défauts identifiés, donc nous sommes gagnant-gagnant !
Les contournements sont schématiquement:
1. réutiliser ce code va pouvoir, par une librairie de spécification
2. ne pas l'exécuter en production (paradoxal et intriguant, n'est-ce-pas ?)
Conclusion
Nous avons vu l'ensemble des techniques et mis en place des fonctions de base pour pouvoir contractualiser l'usage de Pycairo. Cette approche nous permet une bonne factorisation et modularisation des contrats, indispensable à leur maintenabilité au long terme.
Comme vous l'avez sans doutes remarqué, l'approche est possède des particularismes qui en font une spécialité. En tant que développeur, il s'agit d'une nouvelle compétence à acquérir. Les bénéfices ? Avec le recul il sont clairs pour moi: nouveau point de vue sur le code, nouvelles techniques, permettant de tester, débugger, optimiser son code de manière plus efficace.
Dans les prochains posts, je montrerait la suite du code, en donnant un aperçu d'autres fonctions et techniques plus exotiques, et en détaillant l'usage avec un système externe: le système de fichiers.
Et vous, quelles techniques aimez vous utiliser pour prendre en main une nouvelle librairie ? J'ai activé les commentaires, alors n'hésitez pas à demander ci-dessous.
Mini Q&A
J'ai reçu d'autres questions de votre part
- Les contrats ressemblent quand même à du code. Et du coup dépendamment de la complexité, ça reste du code. Par exemple tu fais des
count()
d'un mot la dedans, ça pourrait presque être une fonction du business.- Tout à fait. La principale différence s'il y en a une va résider dans son implémentation. Lorsqu'on a deux implémentation: celle du business va être efficace, optimisée; tandis que celle des contrats, sera lisible, expressive et ne lèvera pas d'exceptions.
- Les contrats sont verbeux, il faut les maintenir, ils prennent de la place a l’écran
- Oui. C'est aussi vrai des commentaires et les tests également. En ajoutant des contrats le volume de commentaires et de tests diminue en proportion. Et je pense que c'est un bon compromis d'avoir ces 3 approches ensemble, pour ne pas mettre tous ses œufs dans le même panier.
Ce ne sont pas du tout les seules questions que j'ai reçu mais ce billet est déjà bien long, je garde la réponse au reste pour un prochain billet 😉
Liens
Le code est présent sur le repository du framework de design-by-contract que j'ai crée.

Quelques ressources discutées dans l'article


