Développer en Python #################### .. contents:: Table des matières :local: .. 1 Le zen du Python 1.1 Us et coutumes 1.2 Principes de conception logicielle 2 Développement piloté par les tests 3 Outils de développement 3.1 *Integrated Development Environment* 3.2 Vérification du code 3.3 Débogage 3.4 Profilage et optimisation 3.5 Documentation 3.6 *Python packages* 3.7 Système de gestion de versions 3.8 Intégration continue .. _td7: Le zen du Python ================ .. index:: Zen du Python Le *zen du Python* (:pep:`20`) est une série de 20 aphorismes [#zen]_ donnant les grands principes du Python:: >>> import this 1. Beautiful is better than ugly. 2. Explicit is better than implicit. 3. Simple is better than complex. 4. Complex is better than complicated. 5. Flat is better than nested. 6. Sparse is better than dense. 7. Readability counts. 8. Special cases aren't special enough to break the rules. 9. Although practicality beats purity. 10. Errors should never pass silently. 11. Unless explicitly silenced. 12. In the face of ambiguity, refuse the temptation to guess. 13. There should be one-- and preferably only one --obvious way to do it. 14. Although that way may not be obvious at first unless you're Dutch. 15. Now is better than never. 16. Although never is often better than *right* now. 17. If the implementation is hard to explain, it's a bad idea. 18. If the implementation is easy to explain, it may be a good idea. 19. Namespaces are one honking great idea -- let's do more of those! Une traduction libre en français: 1. Préfèrer le beau au laid, 2. ... l’explicite à l’implicite 3. ... le simple au complexe, 4. ... le complexe au compliqué, 5. ... le déroulé à l’imbriqué, 6. ... l’aéré au compact. 7. La lisibilité compte. 8. Les cas particuliers ne le sont jamais assez pour violer les règles, 9. ... même s'il faut privilégier la praticité à la pureté. 10. Ne jamais passer les erreurs sous silence, 11. ... ou les faire taire explicitement. 12. En cas d’ambiguïté, résister à la tentation de deviner. 13. Il devrait y avoir une -- et de préférence une seule -- façon évidente de procéder, 14. ... même si cette façon n’est pas évidente à première vue, à moins d’être Hollandais. 15. Mieux vaut *maintenant* que *jamais*, 16. ... même si *jamais* est souvent préférable à *immédiatement*. 17. Si l’implémentation s’explique difficilement, c’est une mauvaise idée. 18. Si l’implémentation s’explique facilement, c’est peut-être une bonne idée. 19. Les espaces de noms sont une sacrée bonne idée, utilisons-les plus souvent ! Us et coutumes -------------- - *Keep it simple, stupid!* - *Don't repeat yourself.* - *Fail early, fail often, fail better!* (`raise`) - *Easier to Ask for Forgiveness than Permission* (`try ... except`) - *We’re all consenting adults here.* (attributs privés) .. rubric:: Quelques conseils supplémentaires: - « *Don't reinvent the wheel, unless you plan on learning more about wheels* » (Jeff Atwood): cherchez si ce que vous voulez faire n'a pas déjà été fait (éventuellement en mieux...) pour vous concentrer sur *votre* valeur ajoutée, réutilisez le code (en citant évidemment vos sources), améliorez le, et contribuez en retour si possible! - Écrivez des programmes pour les humains, pas pour les ordinateurs: codez *proprement*, structurez vos algorithmes, commentez votre code, utilisez des noms de variable qui ont un sens, soignez le style et le formatage, etc. - *Code is read far more often than it is written.* Ne croyez pas que vous ne relirez jamais votre code (ou même que personne n'aura jamais à le lire), ou que vous aurez le temps de le refaire mieux plus tard... - *You ain't gonna need it*: se concentrer sur les fonctionnalités nécessaires plutôt que de prévoir d'emblée l'ensemble des cas. - « *Premature optimization is the root of all evil* » (Donald Knuth): mieux vaut un code lent mais juste et maintenable qu'un code rapide et faux ou incompréhensible. Dans l'ordre absolu des priorités: 1. *Make it work.* 2. *Make it right.* 3. *Make it fast.* - *Respectez* le zen du python, il vous le rendra. .. rubric:: Voir également: - le *Style Guide for Python Code* (:pep:`8`) - `Google Python Style Guide `_ - `The Best of the Best Practices (BOBP) Guide for Python `_ - The hitchhiker's guide to Python `Code Style `_ - `les secrets d'un code pythonique `_ |fr| Principes de conception logicielle ---------------------------------- La bonne conception d'un programme va permettre de gérer efficacement la complexité des algorithmes, de faciliter la maintenance (p.ex. correction des erreurs) et d'accroître les possibilités d'extension. **Modularité** Le code est structuré en répertoires, fichiers, classes, méthodes et fonctions. Les blocs ne font pas plus de quelques dizaines de lignes, les fonctions ne prennent que quelques arguments, la structure logique n'est pas trop complexe, etc. En particulier, le code doit respecter le *principe de responsabilité unique*: chaque entité élémentaire (classe, méthode, fonction) ne doit avoir qu'une unique raison d'exister, et ne pas tenter d'effectuer plusieurs tâches sans rapport direct (p.ex. lecture d'un fichier de données *et* analyse des données). **Flexibilité** Une modification du comportement du code (p.ex. l'ajout d'une nouvelle fonctionnalité) ne nécessite de changer le code qu'en un nombre restreint de points. Un code *rigide* devient rapidement difficile à faire évoluer, puisque chaque changement requiert un grand nombre de modifications. **Robustesse** La modification du code en un point ne change pas de façon inopinée le comportement dans une autre partie *a priori* non reliée. Un code *fragile* est facile à modifier, mais chaque modification peut avoir des conséquences inattendues et le code tend à devenir instable. **Réutilisabilité** La réutilisation d'une portion de code ne demande pas de changement majeur, n'introduit pas trop de dépendances, et ne conduit pas à une duplication du code. L'application de ces principes de développement dépend évidemment de l'objectif final du code: - une bibliothèque de bas niveau, utilisée par de nombreux programmes (p.ex. :mod:`numpy`), favorisera la robustesse et la réutilisabilité aux dépends de la flexibilité: elle devra être particulièrement bien pensée, et ne pourra être modifiée qu'avec parcimonie; - inversement, un script d'analyse de haut niveau, d'utilisation restreinte, pourra être plus flexible mais plus fragile et peu réutilisable. .. _TDD: Développement piloté par les tests ================================== Le *Test Driven Development* (TDD, ou en français « développement piloté par les tests ») est une méthode de programmation qui permet d'éviter des bogues *a priori* plutôt que de les résoudre *a posteriori*. Ce n'est pas une méthode propre à Python, elle est utilisée très largement par les programmeurs professionnels. Le cycle préconisé par TDD comporte cinq étapes : 1. Écrire un premier test ; 2. Vérifier qu'il échoue (puisque le code qu'il teste n'existe pas encore), afin de s'assurer que le test est valide et exécuté ; 3. Écrire un code minimal pour passer le test ; 4. Vérifier que le test passe correctement ; 5. Éventuellement « réusiner » le code (*refactoring*), c'est-à-dire l'améliorer (rapidité, lisibilité) tout en gardant les mêmes fonctionnalités. «~Diviser pour mieux régner~»: chaque fonction, classe ou méthode est testée indépendemment. Ainsi, lorsqu'un nouveau morceau de code ne passe pas les tests qui y sont associés, il est certain que l'erreur provient de cette nouvelle partie et non des fonctions ou objets que ce morceau de code utilise. On distingue ainsi hiérarchiquement: 1. Les tests unitaires vérifient individuellement chacune des fonctions, méthodes, etc.; 2. Les tests d'intégration évaluent les interactions entre différentes unités du programmes; 3. Les tests système assurent le bon fonctionnement du programme dans sa globalité. Il est très utile de transformer toutes les vérifications réalisées au cours du développement et du débogage sous forme de tests, ce qui permet de les réutiliser lorsque l'on veut compléter ou améliorer une partie du code. Si le nouveau code passe toujours les anciens tests, on est alors sûr de ne pas avoir cassé les fonctionnalités précédentes (régréssions). Nous avons déjà vu aux TD précédents plusieurs façons de rédiger des tests unitaires. * Un *doctest* est un exemple (assez simple) d'exécution de code inclus dans la *docstring* d'une classe ou d'une fonction: .. literalinclude:: mean_power.py :pyobject: mean_power :emphasize-lines: 12-15 :linenos: Les *doctests* peuvent être exécutés de différentes façons (voir ci-dessous): - avec le module standard :mod:`doctest`: `python -m doctest -v mean_power.py` - avec pytest_: `py.test --doctest-modules -v mean_power.py` - avec nose_: `nosetests --with-doctest -v mean_power.py` * Les fonctions dont le nom commence par `test_` et contenant des `assert` sont automatiquement détectées par pytest_ [#py.test]_. Cette méthode permet d'effectuer des tests plus poussés que les *doctests*, éventuellement dans un fichier séparé du code à tester. P.ex.: .. literalinclude:: ../Exercices/animaux.py :start-after: start-tests :end-before: end-tests :linenos: Les tests sont exécutés via :samp:`py.test {programme.py}`. * Le module :mod:`unittest` de la bibliothèque standard permet à peu près la même chose que pytest_, mais avec une syntaxe souvent plus lourde. :mod:`unittest` est étendu par le module non-standard nose_. Outils de développement ======================= Je fournis ici essentiellement des liens vers des outils pouvant être utiles pour développer en Python. .. _ide: *Integrated Development Environment* ------------------------------------ - :ref:`idle `, l'IDE intégré à Python - :program:`emacs` + `python-mode` pour l'édition, et :program:`ipython` pour l'execution de code (voir `Python Programming In Emacs `_) - `spyder `_ - `pyCharm `_ (la version `community` est gratuite) - `eclipse-pydev `_ - `Visual Studio `_ - `10 Best Python IDE & Code Editors `_ - `List of IDEs for Python `_ Vérification du code -------------------- Il s'agit d'outils permettant de vérifier *a priori* la validité stylistique et syntaxique du code, de mettre en évidence des constructions dangereuses, les variables non-définies, etc. Ces outils ne testent pas nécessairement la validité des algorithmes et de leur mise en oeuvre... - :pypi:`pycodestyle` (ex-`pep8`) et :pypi:`autopep8` - :pypi:`pyflakes` - :pypi:`pychecker` - :pypi:`pylint` Débogage -------- Les débogueurs permettent de se « plonger » dans un code en cours d'exécution ou juste après une erreur (analyse post-mortem). - Module de la bibliothèque standard: :mod:`pdb` Pour déboguer un script, il est possible de l'exécuter sous le contrôle du débogueur :mod:`pdb` en s'interrompant dès la 1re instruction:: python -m pdb script.py (Pdb) Commandes (très similaires à `gdb `_): * :samp:`h[elp] {[command]}`: aide en ligne; * :samp:`q[uit]`: quitter; * :samp:`r[un] {[args]}`: exécuter le programme avec les arguments; * :samp:`d[own]/u[p]`: monter/descendre dans le stack (empilement des appels de fonction); * :samp:`p {expression}`: afficher le résultat de l'expression (`pp`: *pretty-print*); * :samp:`l[ist] {[first[,last]]}`: afficher le code source autour de l'instruction courante (`ll`: *long list*); * :samp:`n[ext]/s[tep]`: exécuter l'instruction suivante (sans y entrer/en y entrant); * :samp:`unt[il]`: continuer l'exécution jusqu'à la ligne suivante (utile pour les boucles); * :samp:`c[ont[inue]]`: continuer l'exécution (jusqu'à la prochaine interruption ou la fin du programme); * :samp:`r[eturn]`: continuer l'exécution jusqu'à la sortie de la fonction; * :samp:`b[reak] {[[filename:]lineno | function[, condition]]}`: mettre en place un point d'arrêt (`tbreak` pour un point d'arrêt *temporaire*). Sans argument, afficher les points d'arrêts déjà définis; * :samp:`disable/enable {[bpnumber]}`: désactiver/réactiver tous ou un point d'arrêt; * :samp:`cl[ear] {[bpnumber]}`: éliminer tous ou un point d'arrêt; * :samp:`ignore {bpnumber [count]}`: ignorer un point d'arrêt une ou plusieurs fois; * :samp:`condition {bpnumber}`: ajouter une condition à un point d'arrêt; * :samp:`commands {[bpnumber]}`: ajouter des instructions à un point d'arrêt. - Commandes :program:`ipython`: :samp:`%run {monScript.py}`, `%debug`, `%pdb` Si un script exécuté sous :program:`ipython` (commande `%run`) génère une exception, il est possible d'inspecter l'état de la mémoire au moment de l'erreur avec la commande `%debug`, qui lance une session :mod:`pdb` au point d'arrêt. `%pdb on` lance systématiquement le débogueur à chaque exception. L'activité de débogage s'intégre naturellement à la nécessité d'écrire des tests unitaires: 1. trouver un bogue; 2. écrire un test qui aurait du être validé en l'absence du bogue; 3. corriger le code jusqu'à validation du test. Vous aurez alors au final corrigé le bug, *et* écrit un test s'assurant que ce bogue ne réapparaîtra pas inopinément. .. _ProfOpt: Profilage et optimisation ------------------------- .. Warning:: *Premature optimization is the root of all evil* -- Donald Knuth Avant toute optimisation, s'assurer extensivement que le code fonctionne et produit les bons résultats dans tous les cas. S'il reste trop lent ou gourmand en mémoire *pour vos besoins*, il peut être nécessaire de l'optimiser. Le :ref:`profilage ` permet de déterminer le temps passé dans chacune des sous-fonctions d'un code (ou ligne par ligne: :pypi:`line profiler `, ou selon l'utilisation de la mémoire: :pypi:`memory profiler `), afin d'y identifier les parties qui gagneront à être optimisées. - `python -O`, `__debug__`, `assert` Il existe un mode « optimisé » de python (option `-O`), qui pour l'instant ne fait pas grand chose (et n'est donc guère utilisé....): * la variable interne `__debug__` passe de `True` à `False`; * les instructions `assert` ne sont pas évaluées. - :mod:`timeit` et :samp:`%timeit {statement}` sous :program:`ipython`:: In [1]: def t1(n): ...: l = [] ...: for i in range(n): ...: l.append(i**2) ...: return l ...: ...: def t2(n): ...: return [ i**2 for i in range(n) ] ...: ...: def t3(n): ...: return N.arange(n)**2 ...: In [2]: %timeit t1(10000) 2.7 ms ± 12.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) In [3]: %timeit t2(10000) 2.29 ms ± 13.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) In [4]: %timeit t3(10000) 15.9 µs ± 120 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) - :mod:`cProfile` et :mod:`pstats`, et :samp:`%prun {statement}` sous :program:`ipython`:: $ python -m cProfile calc_pi.py 3.1415925580959025 10000005 function calls in 4.594 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 1.612 1.612 4.594 4.594 calc_pi.py:10(approx_pi) 1 0.000 0.000 4.594 4.594 calc_pi.py:5() 10000000 2.982 0.000 2.982 0.000 calc_pi.py:5(recip_square) 1 0.000 0.000 4.594 4.594 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {built-in method builtins.print} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} - `Tutoriel de profilage `_ Une fois identifiée la partie du code à optimiser, quelques conseils généraux: * en cas de doute, favoriser la lisibilité aux performances; * utiliser des opérations sur les tableaux, plutôt que sur des éléments individuels (*vectorization*): listes en compréhension, tableaux :mod:`numpy` (qui ont eux-mêmes été optimisés); * `cython `_ est un langage de programmation **compilé** très similaire à python. Il permet d'écrire des extensions en C avec la facilité de python (voir notamment `Working with Numpy `_); * `numba `_ permet *automagiquement* de compiler *à la volée* (:abbr:`JIT(Just In Time)`) du pur code python via le compilateur `LLVM `_, avec une optimisation selon le CPU (éventuellement le GPU) utilisé, p.ex.:: from numba import jit # compilation à la volée (seulement au 1e appel) @jit def crible(n): ... ou:: from numba import guvectorize # ufunc numpy compilée @guvectorize(['void(float64[:], intp[:], float64[:])'], '(n),()->(n)') def move_mean(a, window_arr, out): ... * à l'avenir, l'interpréteur CPython actuel sera éventuellement remplacé par `pypy `_, basé sur une compilation :abbr:`JIT(Just In Time)`. .. rubric:: Lien: `Performance tips `_ Documentation ------------- * Outils de documentation, ou comment transformer *automagiquement* un code-source bien documenté en une documentation fonctionnelle. - `Sphinx `_; - `reStructuredText for Sphinx `_; - `Awesome Sphinx `_; - `apidoc `_ (documentation automatique). * Conventions de documentation: - *Docstring convention*: :pep:`257`; - `Documenting Your Project Using Sphinx `_; - `A Guide to NumPy/SciPy Documentation `_; - `Sample doc `_ (matplotlib). .. rubric:: Lien: `Documentation Tools `_ *Python packages* ----------------- Comment installer/créer des modules externes: - `pip `_; - :rtfd:`Hitchhiker’s Guide to Packaging `; - `Packaging Python Projects `_ - `Packaging a python library `_; - :rtfd:`cookiecutter` est un générateur de squelettes de projet via des *templates* (pas uniquement pour Python); - :rtfd:`cx-freeze`, pour générer un exécutable à partir d'un script. Système de gestion de versions ------------------------------ La gestion des versions du code permet de suivre avec précision l'historique des modifications du code (ou de tout autre projet), de retrouver les changements critiques, de développer des branches alternatives, de faciliter le travail collaboratif, etc. `Git `_ est un :abbr:`VCS (Version Controling System)` particulièrement performant (p.ex. utilisé pour le développement du `noyau Linux `_ [#gitW]_). Il est souvent couplé à un dépôt en ligne faisant office de dépôt de référence et de solution de sauvegarde, et offrant généralement des solutions d'intégration continue, p.ex.: * les très célèbres `GitHub `_ et `GitLab `_, gratuits pour les projets libres; * pour des projets liés à votre travail, je conseille plutôt des dépôts directement gérés par votre institution, p.ex. `GitLab-IN2P3 `_. Git_ mérite un cours en soi, et devrait être utilisé très largement pour l'ensemble de vos projets (p.ex. rédaction d'articles, de thèse de cours, fichiers de configuration, tests numériques, etc.). Quelques liens d'introduction: - `Pro-Git book `_, le livre « officiel »; - `Git Immersion `_; - `Git Magic `_. Intégration continue -------------------- L'intégration continue est un ensemble de pratiques de développement logiciel visant à s'assurer de façon systématique que chaque modification du code n'induit aucune *régression*, et passe l'ensemble des tests. Cela passe généralement par la mise en place d'un système de gestion des sources, auquel est accolé un mécanisme automatique de compilation (*build*), de déploiement sur les différentes infrastructures, d'éxecution des tests (unitaires, intégration, fonctionnels, etc.) et de mise à disposition des résultats, de mise en ligne de la documentation, etc. La plupart des développements des logiciels *open source* majeurs se fait maintenant sous intégration continue en utilisant des services en ligne directement connectés au dépôt source. Exemple sur `Astropy`: * `Travis CI `_ intégration continue; * `Coveralls `_ taux de couverture des tests unitaires; * :rtfd:`Readthedocs ` documentation en ligne; * `Depsy `_ mise en valeur du développement logiciel dans le monde académique (*measure the value of software that powers science*, **non maintenu**). .. rubric:: Notes de bas de page .. [#zen] Dont seulement 19 ont été écrits. .. [#py.test] pytest_ ne fait pas partie de la bibliothèque standard. Il vous faudra donc l'installer indépendemment si vous voulez l'utiliser. .. [#gitW] Et maintenant du `code Windows `_! .. _pytest: https://docs.pytest.org/ .. _nose: https://nose.readthedocs.io/ .. |fr| image:: ../_static/france_flag_icon.png :alt: Fr .. |en| image:: ../_static/uk_flag_icon.png :alt: En