Python, du script au développement

Python, du script au développement

J’ai très souvent tendance à mettre (un peu outrageusement) en avant le langage Python, que ce soit à travers des petites piques plus ou moins discrètes ou via des articles consacrés. Cet article en est un nouveau, mais cependant j’ai cette fois l’ambition de prendre un peu le contre-pied de l’admiration béate, notamment en invectivant certaines pratiques que j’estime à termes préjudiciables pour le langage.

Le Python est un langage facile à prendre en  main. Tout le monde le dit, c’en devient même presque la justification première de son utilisation. Il suffit d’écrire dans un fichier un shebang et quelques lignes qui semblent naturelles, un chmod +x et c’est partie ! On a notre « Hello World! ».
Mais le Python n’est pas un langage simple. Au contraire, il est extraordinairement complexe et subtil.

Le problème est que, dans les milieux où on n’a pas le temps et/ou la volonté d’étudier les langages, l’ensemble des personnes intervenant sur des projets Python sont persuadées que c’est facile. Les managers prévoient des plannings courts et provisionnent des ressources souvent débutantes et les développeurs font ce qu’ils ont toujours fait : du Python facile, à plat, bancal. Souvent les projets n’ont pas d’architecte logiciel, et ce sont les développeurs qui créent l’architecture au fil du développement.

Python est beaucoup utilisé dans des cas peu contraignants, notamment des wrappers sur des bibliothèques C, C++, Matlab ou Fortran, et des interfaces (API, GUI et beaucoup WebUI). Dans ces cas, le code amateur fait illusion, c’est pas super réactif, ça plante de temps en temps mais il suffit de redémarrer le service et c’est de nouveau debout, en général sans gros impact sur les calculs sous-jacents.
Malheureusement, au bout d’un moment les managers sont convaincus de posséder plusieurs ressources Python expérimentées voire expertes, et alors on commence à demander du développement industriel en Python pour des applications de cœur de métier…

Objectivement, Python a de beaux arguments pour être utilisé dans le développement industriel. Le langage et sa communauté ont depuis longtemps prouvé qu’ils étaient mûrs, fiables et efficaces. Le langage évolue de façon sérieuse et rigoureuse, à travers les PEP par exemple. Le travail sur les interpréteurs est réellement excellent et offre des performances remarquables au vue des contraintes fortes du langage. De plus, de part sa modularité au niveau de l’interpréteur justement, le langage peut facilement se porter dans des environnements d’exécutions multiples, d’abord au niveau du système d’exploitation évidemment, mais aussi pour ce qui est d’exécuter le programme, avec des passerelles vers du bytecode Java, du C/C++, du JavaScript, du .NET, etc.
De plus, Python est, s’il est bien écrit, extrêmement clair (c’est pour moi, l’outil informatique qui illustre le mieux la philosophie KISS). Il bénéficie d’une palette de bibliothèques de très bonnes qualités, et pas seulement pour le calcul scientifique. L’ensemble des outils gravitant autour de Python (frameworks, versionning, terminal) est lui aussi très riche. Un bémol cependant : Python n’a pas encore trouvé un puissant éditeur dédié (autre que Emacs, bien sûr).
Enfin, Python est massivement utilisé dans le cloud computing et la cybersécurité, qui sont comme on le sait (un peu trop sûrement) les probables futurs grands domaines de croissance de l’informatique.

Mais voilà, comme on peut l’entendre dans tant de mauvais films, lorsqu’un jeune bellâtre persécuté se découvre des pouvoirs après [s’être fait mordre par une [araignée | un vampire | un loup-garou ] | avoir subit le rayonnement d’une quelconque substance forcément radioactive – et verte de préférence | avoir fait un pacte avec le diable ] et vient chercher conseil auprès d’une quelconque figure plus ou moins paternelle, cette dernière (ça a beau être paternelle, ça reste une figure) commence souvent sa réplique par : « Un grand pouvoir implique de grandes responsabilités » [1].
Le problème de Python est qu’il donne réellement un grand pouvoir, mais ce pouvoir est souvent utilisé de façon imprécise, sans maîtrise véritable de l’outil.

Je demandais récemment au chef du pôle de dev de ma boîte pourquoi il voulait me faire intervenir sur des projets Python ni complexes ni intéressants, lui opposant naïvement que « tout le monde sait coder en Python ». Il m’a répondu : « tout le monde sait scripter en Python ». Je me suis souvenu du code que je devais relire et réécrire chaque jour : des fichiers de plusieurs milliers de lignes, des fonctions de 400 lignes, une syntaxe aléatoire, des « optimisations » douteuses, pas de classes (dans tous les sens du terme), pas de factorisation, pas de tests, pas de réflexion sur le code et les algorithmes.

Je prends aussi ces critiques pour moi-même : j’ai personnellement commencé sérieusement le Python sur un PoC assez important avec beaucoup de libertés. On savait ce qu’on faisait, on avait nos objectifs, et, malgré quelques pierres d’achoppement, on contrôlait techniquement notre sujet. Le projet a rapidement donné des résultats très sympas, les fonctionnalités se multipliaient et on créait ce qui nous semblait être (et qui l’était peut-être, mais je ne peux pas juger objectivement) de l’innovation.
Au bout d’un an environ, nous avions atteint un « plus haut » dans le projet : les objectifs étaient atteints, les difficultés techniques levées, les performances honnêtes sinon bonnes, l’ergonomie correcte.
L’étape suivante logique aurait été une industrialisation. Pour différentes raisons, il n’en fut rien, on nous demanda simplement de continuer à enrichir notre PoC.
À partir de là, ce fut la descente aux enfers. Nous avons commencé à avoir beaucoup de mal à ajouter des fonctionnalités : chaque modification faisait sauter le code à l’autre bout du programme, le code et sa structure était incompréhensible, on tombait dans des états incohérents qu’il fallait laborieusement déboguer à la main. Fort de toute l’expérience et du recul apportés par des mois de développement, nous avons réfléchit à la logique de notre programme, à nos algorithmes, nous avons profondément modifié l’architecture, plusieurs fois, pour la rationaliser et factoriser du code, mais nous n’avons pu faire le nettoyage jusqu’au bout, car il fallait toujours intégrer des fonctionnalités. Nous avons essayé de faire des tests automatiques, mais le code était beaucoup trop dense, c’était tout simplement impossible.
Après un peu plus de deux ans, le projet fut arrêté. Une délivrance : le programme n’était plus viable, instable, le moindre développement aboutissait à des régressions fonctionnelles et demandait alors des semaines de débogage. Un bilan terrible : l’absence de cadre sérieux avait seule tué le projet.

Cette expérience m’a rendu particulièrement sensible aux problématiques de développement efficace. J’ai eu la chance après ça de travailler sous un gourou avec une belle connaissance des process de développement industriel en environnement d’intégration continue, et pas du tout du genre à faire des concessions dessus.
Ses préceptes sont assez simples :

  • il doit être possible, à tout moment, de produire une version stable (sûrement incomplète, mais stable) du programme;
  • priorité absolue à la stabilité : si un seul indicateur dans l’environnement d’intégration continue échoue, on arrête tout développement pour analyser et corriger le problème;
  • un bug n’est pas considéré corrigé s’il n’y a pas de tests permettant à l’avenir de révéler ce bug;
  • tout ajout de code doit passer par ces étapes :
    1. écriture du/des tests reflétant le comportement attendu,
    2. développement du code,
    3. validation par les tests (réécriture du code/du/des tests si besoin),
    4. factorisation.
  • La couverture de tests doit être maximale, et un commit poussé signifie intrinsèquement que tous les tests passent pour ce commit.
  • Le code est composé de deux parties principales : la première au contact avec l’extérieur (utilisateur, bdd, fichiers, autres programmes, etc.) qui doit être blindée et qui nourrit en données de confiance la seconde, interne au programme, et qui au contraire doit planter à la moindre dérive (ce qui permet de souligner les défauts d’implémentation et/ou les cas non traités par la première catégorie).
  • il doit être impossible, en lisant le code, de pouvoir identifier son auteur.

À part le dernier sur lequel j’ai des réticences (je préfère une approche plus « artisanale », avec un vrai apport de l’expertise – et donc aussi, quand ça casse, de la responsabilité – de chacun directement visible dans le code), ces points sont dans l’esprit assez naturels et sains. Mais ils reflètent un idéal qu’on ne respecte pas, d’abord nous développeurs par paresse et/ou ignorance, et ensuite les managers en priorisant l’ajout de fonctionnalités qui brillent par rapport à la factorisation et le nettoyage de code.

Nous partons donc de cet idéal fiers, guillerets et naïfs, pour arriver dans la bonne pratique du développement qui sent la vieille sueur rance… Et quoi de mieux pour illustrer les dérives possibles que de me baser sur des citations (véridiques – du moins dans le fond) que j’ai pu entendre dans mes projets ?

« Je ferais les tests au prochain commit ! »

On connait tous notre tendance à la procrastination. Remettre des tests à plus tard et le meilleur moyen de ne jamais les faire. Et quand il faut s’y mettre, il y en a tellement à faire, et les modifications de code qui viennent avec sont tellement importantes que ça en devient franchement déplaisant.
Faire des tests au fil de l’eau est extrêmement sain, pour des tonnes de raisons :

  • les tests, s’ils sont durs à écrire, permettent de voir immédiatement où le code est complexe et on en profite pour créer des nouvelles méthodes plus simples. Plus globalement, ils permettent de prendre un peu de recul sur notre code tout jeune, ce qui aide à l’améliorer;
  • en faisant des tests (qui passent), on capitalise une confiance inouïe sur ce qu’on fait. Non seulement les tests permettent de voir tout de suite où on a développé n’importe comment, mais ils permettent aussi à l’inverse de valider un bon travail. Quand on arrive à une base honorable de tests que l’on sait pertinents (pour les avoir écrit) avec une bonne couverture et qu’ils passent tous, là on a des garanties tangibles sur la solidité du code, et on push l’esprit apaisé;
  • toujours sur la confiance, les tests font gagner un temps énorme : plus besoin de déployer et lancer l’application, de cliquer à 2-3 endroits pour voir si notre modification est effective. Les tests passent, ça suffit.

Je ne vous dis pas tout ça parce que je suis embrigadé par le très puissant Lobby Pour Un Monde Mieux Testé. Je le dis parce qu’avant ça me faisait chier, et puis j’en ai fait, et maintenant, quand je code, si les tests passent, je n’essaie même pas de lancer le programme, je le considère comme stable les yeux fermés. Et ça ne m’est encore jamais retourné dans la gueule.

« On va pas faire des classes, ya trois fois rien à coder ! »

001xq5d7Non.

Tu fais des classes.

Ça va pas ralentir le code.
Ça va pas le rendre incompréhensible.

Tu fais des classes.

Sérieusement, quand on me dit ça, je ne sais jamais trop quoi répondre. En fait, je comprends quelque chose comme : « je suis sur un projet de dev et je vais le saboter en faisant du script ». Comme en général chaque collègue lance une cinquantaine de trolls par jour, je pense à une blague, et pourtant … non.

Je crois que tous les sites / threads / forums du monde dans lesquels on trouve le mot « Python » et le mot « Class » disent qu’il faut les utiliser. Je ne vais pas perdre votre temps à reprendre ces arguments, à part un : celui de la maîtrise de la complexité (algorithmique) du code.
Idéalement, chaque classe matérialise un concept simple, si besoin en s’appuyant sur d’autres classes. Le résultat, c’est qu’il doit être trivial pour chacun de ces éléments de déterminer la complexité de leurs méthodes, et donc du programme entier. Il y a trop de composants qu’on ne maîtrise pas dans un programme informatique pour qu’on puisse se payer le luxe de perdre la main sur son propre code.
En Python, les classes répondent infiniment mieux à cette problématique de maîtrise de la complexité que des bibliothèques de fonctions à plat.

Pour ça et pour tout le reste, tu fais des classes.

« Ça, le framework le fait en 2 lignes »

Avant j’aimais bien les frameworks. En Python, la plupart (comme Django ou Flask) arrivent avec un peu tout à portée de main. Gestion des utilisateurs, gestion de l’API, de la bdd, génération de pages d’admin, etc., tout est simple, tout est beau.

Et puis j’ai voulu faire du code un peu plus sérieux. Depuis, j’ai beaucoup plus tendance à avoir des coups de sang contre les frameworks que de leur être reconnaissant.

Un framework, c’est une boîte noire [2], on ne sait pas comment ça marche, c’est un environnement auquel on ne peut pas faire confiance. C’est le monde extérieur, et pour s’en protéger, il y a deux choses : blinder les échanges avec lui et surtout minimiser au maximum l’adhérence de notre code avec. Sans ça, on va droit vers une forte dépendance au framework qui est extrêmement néfaste, ne serait-ce que dans le cas (pas si improbable) que le projet porteur du framework soit abandonné.

Pour plus de détail là-dessus, je vous invite à patienter quelques mois, le temps que j’écrive un petit brûlot sur Flask qui m’a posé de vrais problèmes de fond et qui sous ses dessous affriolants cache des trucs franchement gênants…

« C’est vraiment pas grand chose, on le rajoute pour la release. »

C’est en général la phrase qui indique qu’un projet est condamné. En soit, ça semble mineur, car effectivement, souvent ce n’est pas grand chose. Mais ça souligne une dérive  dangereuse : du moment qu’on accepte un unique ajout par rapport à ce qui est prévu, tout ce qui a été planifié ne vaut plus rien.
Du point de vue d’un manager, un petit ajout de 5 min équivaut surtout à la fonctionnalité qui fait la différence par rapport au concurrent. Et le manager, dans sa tête, il n’a pas une fonctionnalité qui fait la différence, il en a trois milles. Et il va arriver à toutes les introduire, parce qu’il prend la pression du client, ou du commercial, ou du boss, et parce que c’est son boulot de challenger (un peu) ses gars.

Résultat : comme on ne récupère pas du temps sur les fonctionnalités déjà prévues, on taille dans le reste : tests, nettoyage de code, refactoring. Du travail essentiel, je le souligne encore, mais à court terme, c’est vrai, ça ne clignote pas, ça ne fait pas un « beep » rassurant, ça ne permet pas d’avoir une image à côté de son login.

Avec ce genre de fonctionnement, le projet va inexorablement à sa fin, avec des pentes différentes selon la virtuosité des développeurs, mais l’idée globale est qu’à la première release, tout est fonctionnellement au niveau, avec même des petites trucs non prévues en plus (ce qui va encore plus renforcer à l’avenir la propension à ajouter des tâches non planifiées) ; à la deuxième, ça peut tenir, il y a toujours quelques fonctionnalités en plus, mais les gars près du terrain sentent que le code commence à avoir de l’inertie. À la troisième, c’est vraiment la galère de travailler, on commence à passer un temps non négligeable sur du code qui vieillit mal; et après ça on n’en parle plus…

65498798Ici, le job de l’équipe, et en premier lieu du responsable technique s’il y en a un, c’est de calmer le gars qui ajoute des trucs pas prévus.
Si c’est un développeur un peu fou, ultra motivé, ça doit pouvoir se cadrer assez facilement, à l’équipe de le catalyser vers les choses vraiment bénéfiques au projet.
Si c’est un manager, ou un supérieur (ou les deux), c’est plus délicat… Mais il faut encaisser le choc, et typiquement un bon respo technique a parmi ses taches les plus importantes de protéger son équipe de ce genre de pression malsaine, en jouant de son aura / de son autorité pour refuser des ajouts impromptus. Ce n’est parfois pas très bien vu de s’opposer systématiquement à ce genre de demandes, et c’est d’autant plus rageant qu’on sait qu’on le fait pour le bien du projet, mais il faut s’y coller. Et tant pis pour l’augmente, on travaille pour la gloire…

Titre pour dire que c’est la fin d’au dessus et le début d’en dessous (et oui c’est délibéré)

Je ne veux pas finir par une vibrante scène où j’appellerai le monde à se concentrer sur la paix et la beauté intérieure.

Mais j’aime le Python, je pense qu’il a sa place dans le développement industriel, et je pense qu’il mérite de devenir un langage de tout premier plan.
Les problématiques soulignées plus haut peuvent être transposées à n’importe quel langage auquel on soumettrait une logique industrielle. Cependant dans le cas du Python, l’immense souplesse du langage fait qu’on se rend compte souvent très tard, trop tard, de l’obligation d’être rigoureux.

Je voudrais donc encourager toute personne à qui on demande un jour de faire du développement un peu sérieux en Python, et qui n’en a jamais fait, de prendre du recul sur tout ce qu’elle a pu faire en Python jusqu’à présent. D’être réellement, et de bonne foi, critique sur son propre travail, et de toujours améliorer son code.

Arrêtez de scripter, vous vous faites du mal. Faites du vrai code, devenez des badass.

1. Phrase non contractuelle, peut changer selon le bousin projeté.

2. Je refuse de tomber dans l’argument facile de l’Open Source « visible par tous » etc. etc. J’adore l’Open Source, mais tout le monde sait pertinemment que 99% de leur utilisation se fait sans évaluation ni audit (voir cet article sur l’utilisation ‘facile’ des logiciels open source en partant du cas HeartBleed). Rarement (jamais ?) une entreprise provisionne ne serait-ce qu’une semaine à une équipe pour évaluer et apprendre à maîtriser sérieusement un framework avant de commencer le développement. Alors oui, le réalisme oblige de considérer tout framework (et toute dépendance) comme une boîte noire.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *