Auteur: | Julien Jehannet <julien.jehannet@logilab.fr> |
---|---|
Date: | 2008-05-17 |
Niveau: | Débutant / Confirmé |
Le code est fait pour être lu : quelques pistes pour améliorer la lisibilité et la maintenabilité de votre code python.
Cette présentation parle de quelques bonnes pratiques destinées à mettre toutes les chances de votre côté pour que la maintenance de vos applications ne devienne pas rapidement un cauchemar.
Nous allons au préalable définir rapidement ce qu'est le but de l'assurance qualité; puis nous rappellerons brièvement les principes de bases que tous les développeurs python doivent utiliser. Nous passerons ensuite en revue les outils standards de python pour faciliter la mise au point avant de lister des projets plus ambitieux mais indispensables pour la création de code de qualité.
Pour satisfaire aux contraintes de qualité et fonctionnalités, l'assurance qualité doit viser un contrôle constant sur :
La maîtrise de l'assurance qualité permet au chef de projet (et a fortiori l'équipe de développement) de proposer un nombre de fonctionnalités croissantes (sans régression) dans un cadre stable et évolutif pour la suite du projet.
Cette maîtrise ne peuvent être obtenue que grâce à un code testé en continu et dont les effets sont reproductibles à volonté.
« Il semble que la perfection soit atteinte non quand il n'y a plus rien à ajouter, mais quand il n'y a plus rien à retrancher. »
Antoine de Saint-Exupéry
Outil de gestion de versions (mercurial, subversion, git, ...)
The Zen of Python, by Tim Peters
echo 'import this' >> $PYTHONSTARTUP
« A clever person solves a problem. A wise person avoids it. »
Albert Einstein
Les indispensables:
le module logging
Le module logging contient des fonctions et des classes construisant un système de journalisation flexible pour vos applications
Un logger peut être associé à une sévérité lui indiquant d’ignorer les messages d’importance inférieure à ce niveau
Intérêts
L’envoi d’un message est effectué en appelant une méthode sur une instance de la classe Logger. Ces instances possèdent un nom et sont arrangées selon une hiérarchie en utilisant . comme séparateur Un message est associé à une sévérité (par défaut DEBUG, INFO, WARNING, ERROR ou CRITICAL)
Un logger est associé à une ou plusieurs instances de la classe Handler, qui sont responsables de faire parvenir les messages à une destination particulière
Les handlers permettent une configuration plus poussées de la journalisation: - utilisation de NTEventLogHandler sous Windows - utilisation de SMTPHandler lors d'erreurs critiques seulement (filtre alors sur la sévérité) - utilisation d'un handler pour l'affichage dans une fenêtre d'application
Il est évidemment possible de changer le formattage des messages Vous pouvez mettre au point des règles précise de filtrage avec classe Filter
Tout est configurable par fichier .ini ce qui permet à un administrateur de modifier le comportement sans toucher au code
Exemple simple:
import logging # Python >= 2.4 uniquement logging.basicConfig(level=logging.INFO, format=’%(asctime)s %(levelname)s\t\ %(message)s’) logging.debug(’Un message de debogage’) logging.info("De l’information: %s", ’et hop !’) logging.warning(’Attention’) logger = logging.getLogger(’monappli’) logger.debug(’Un message de debogage de mon appli’) logger.info("Plus d’info: %s", ’hop hop hop!’)
donne à l’exécution :
Pour des exemples plus évolués:
http://www.red-dove.com/python_logging.html
« Programs must be written for people to read, and only incidentally for machines to execute. »
Abelson and Sussman
le module pydoc
Les outils de documentation se basent sur les docstrings qui sont des chaînes de documentation insérées dans le code source de vos programmes python.
La fonction builtin help() utilise le module pydoc et permet d’obtenir de l’aide de manière interactive sur un objet vivant
pydoc est également un script utilisable dans une console shell à la manière des pages de man sous Unix
Intérêts
Les docstrings se placent sur la première ligne suivant les entêtes de classe ou de fonction ou en tête des fichiers contenant des modules. Ce sont de simples textes.
Voir http://www.python.org/dev/peps/pep-0257/
Le module doctest recherche des éléments de textes correspondant à du code en mode intéractif et vérifie son exécution avec le résultat attendu.
En utilisant pydoc, vous vous forcer implicitement à écrire des docstring de qualité car vous savez que ce sera relu
Démonstration du rendu avec "pydoc pydoc"
L'outil epydoc [2] est similaire mais permet de documenter plus en détails avec une génération de graphes de dépendances par exemple.
« Let the machine do the dirty work »
B. W. Kernighan and P. J. Plauger, The Elements of Programming Style
le module unittest
Le module unittest fait partie de la bibliothèque standard Il sert à coder des tests unitaires pour des modules
Intérêts
Voir http://docs.python.org/lib/module-unittest.html
Automatiser vos tests va grandement améliorer les temps de développements tant en garantissant une offre continue des fonctionnalités déjà livrées.
Cela permet de détecter au plus tôt les erreurs
N'oubliez pas que le temps allouer aux vérifications et à la validation est souvent largement sous-estimé et atteint en réalité près de 40%
Code python d'un module calculant une factorielle
from fact import fact import unittest class FactTC(unittest.TestCase): def test_first_results(self): self.assertEquals(fact(1), 1) for n in xrange(2, 10): self.failUnless(fact(n) == n*fact(n-1)) def test_negative_numbers(self): self.assertRaises(ValueError, fact, -12) if __name__ == ’__main__’: unittest.main()
Pour chaque cas, on crée une classe dérivant de TestCase du module unittest
Chaque méthode dont le nom commencera par test sera alors considérée comme un test unitaire (convention)
La fonction main() collecte et lance tous les tests définis dans le module, mais il est possible de donner des arguments sur la ligne de commande pour ne lancer que certains cas de tests
Un test unitaire est considéré comme passé lorsque l’exécution de la méthode n’a pas généré d’erreur (i.e génération d’une exception)
Attention ne pas utiliser le mot clef assert qui est supprimé lors de l’optimisation (python -O)
Exemple d'erreur retournée
$ python unittest_compat.py ............F.. ----------------------------------------------- FAIL: test_all (__main__.Py25CompatTC) ----------------------------------------------- Traceback (most recent call last): File "unittest_compat.py", line 183, in test_all self.assertEquals(irange.next(), 2) File "/usr/lib/python2.3/unittest.py", line 302, in failUnlessEqual raise self.failureException, AssertionError: 1 != 2 ----------------------------------------------- Ran 15 tests in 0.120s FAILED (failures=1)
On distinguera deux types d’erreurs lors de l’exécution d’un test :
« Tester un programme démontre la présence de bugs, pas leur absence »
Edsger Dijkstra
le module pdb
Python dispose d’un débogueur intégré dans la bibliothèque standard, à travers le module pdb
Quand l'utiliser ? Un comportement inattendu apparaît et il est impossible d'écrire un test unitaire pour le caractériser
Lancer l’interpréteur python avec l’option -i permet de passer en interactif lorsqu’une exception remonte jusqu’à l’interpréteur On peut alors exécuter la fonction pm() du module pdb pour analyser l’exception en détail, de la même façon qu’on analyserait un core dump
Plus simple encore, vous pouvez placer ces 2 lignes à n'importe quel endroit de votre code et ceci vous fera rentrer automatiquement dans le débogueur
Code python:
import pdb pdb.set_trace()
Lorsqu’un programme se termine avec une exception, la pile d’appels seule n’est pas toujours suffisante pour comprendre ce qu’il s’est passé
Il y a deux façons d’utiliser ce débogueur: - Exécuter du code pas à pas - Effectuer une analyse post mortem d’un programme après une exception
Lancement du débogueur à la volée:
| > exemple.py(12)?() | -> A = 2 | (Pdb) l | 7 | 8 A = 1 | 9 | 10 pdb.set_trace() | 11 | 12 -> A = 2 | 13 A = 4 | [EOF] | (Pdb) print A | 1 | (Pdb) next | > exemple.py(13)?() | -> A = 4 | (Pdb) print A | 2
Environnement pour les tests unitaires
Couverture de code par les tests
Problème !
On ne couvre que le code qui est exécuté par les tests unitaires...
Nose est intéressant dans le sens où il peut englober les tests de plusieurs utilitaires différents sous forme de plugin
Les outils de couverture de code sont moins efficaces dans le cas de langages dynamiques car seul le code exécuté est testé !
La complexité cyclomatique est un outil de métrologie logiciel développé par Thomas McCabe pour mesurer la complexité d'un programme informatique. Cette mesure comptabilise le nombre de "chemins" au travers d'un programme représenté sous la forme d'un graphe.
Refactoring
Les outils d'analyse de code
Problème ?
La nature dynamique du langage python permet plusieurs approches qui provoquent des résultats différents selon les outils.
Lequel utiliser ?
Les trois évidemment !
La refactorisation du code ne peut intervenir que si un environnement de tests unitaires existe car sinon il est impossible de vérifier que les fonctionnalités sont toujours offertes après la réécriture du code.
Refactorisation <==> toujours à fonctionnalités égales
C'est une étape délicate qui doit rester exceptionnelle. Pour éviter ce type de changement, il peut être utile de s'aider d'outils d'analyse de code.
Ces utilitaires de vérification statique de code pour Python effectuent un travail comparable à celui du vérificateur sémantique d’un compilateur C ou Java.
Pyflakes plus rapide: utile pour intégrer dans votre éditeur et vérifier les erreurs de syntaxe et les imports de modules en trop
PyChecker utilise l'import de module d'où une certaine exécution du code
Pylint est encore trop verbeux par défaut mais il est prévu de corriger ça
Fonctionnalités: Conformité PEP 8 Absence de docstring Variables non utilisées Utilisation d’une variable non initialisée Longueur des lignes Module utilisé mais non importé Mauvais nombre d’arguments passés à une méthode ou une fonction Utilisation de l’opérateur % sur des chaînes avec des formats ne correspondant pas aux arguments Utilisation de méthodes ou d’attributs inexistants Redéfinition d’une fonction/classe/méthode dans la même portée self n’est pas le premier argument d’une méthode Argument de méthode/fonction non utilisé (ignore self) Nommage des fonctions, des méthodes, des variables Complexité du code
La programmation est une création de l'esprit. Elle n'est pas prévisible et donc vouée par essence au changement. L'assurance qualité vous permet de gérer au mieux ces changements en les anticipant et en contrôlant leurs effets.
La seule technique de validation et de vérifications sont les tests. Vous devez prévoir le développement de tel sorte qu'il reste testable. Il est donc important d'isoler les fonctionnalités et d'éviter les dépendances qui ajoutent des cas d'éxecution indésirables
La mailing-liste 'Testing in Python' [3]
PythonTestingToolsTaxonomy wiki page [4]
How to misuse code coverage, Brian Marick (1997) [5]
Working effectively with legacy code, Michael C. Feathers
Refactoring - Improving the design of existing code, Martin Fowler
Refactoring to Patterns, Joshua Kerievsky
Questions ?