VI. Développer en Python▲
VI-A. Le zen du Python▲
Le zen du Python (PEP 20) est une série de 20 aphorismes(7) donnant les grands principes du Python :
>>>
import
this
- Beautiful is better than ugly.
- Explicit is better than implicit.
- Simple is better than complex.
- Complex is better than complicated.
- Flat is better than nested.
- Sparse is better than dense.
- Readability counts.
- Special cases aren't special enough to break the rules.
- Although practicality beats purity.
- Errors should never pass silently.
- Unless explicitly silenced.
- In the face of ambiguity, refuse the temptation to guess.
- There should be one- and preferably only one -obvious way to do it.
- Although that way may not be obvious at first unless you're Dutch.
- Now is better than never.
- Although never is often better than right now.
- If the implementation is hard to explain, it's a bad idea.
- If the implementation is easy to explain, it may be a good idea.
- Namespaces are one honking great idea - let's do more of those!
Une traduction libre en français :
- Préférer le beau au laid.
- L'explicite à l'implicite.
- Le simple au complexe.
- Le complexe au compliqué.
- Le déroulé à l'imbriqué.
- L'aéré au compact.
- La lisibilité compte.
- Les cas particuliers ne le sont jamais assez pour violer les règles.
- Même s'il faut privilégier l'aspect pratique à la pureté.
- Ne jamais passer les erreurs sous silence.
- Ou les faire taire explicitement.
- Face à l'ambiguïté, ne pas se laisser tenter à deviner.
- Il doit y avoir une - et si possible une seule - façon évidente de procéder.
- Même si cette façon n'est pas évidente à première vue, à moins d'être Hollandais.
- Mieux vaut maintenant que jamais.
- Même si jamais est souvent mieux qu'immédiatement.
- Si l'implémentation s'explique difficilement, c'est une mauvaise idée.
- Si l'implémentation s'explique facilement, c'est peut-être une bonne idée.
- Les espaces de nommage sont une sacrée bonne idée, utilisons-les plus souvent !
VI-A-1. Us et coutumes▲
- Fail early, fail often, fail better! (
raise
) - Easier to Ask for Forgiveness than Permission (
try
...except
) - le Style Guide for Python Code (PEP 8)
- Idioms and Anti-Idioms in Python
- Code Like a Pythonista: Idiomatic Python
- Google Python Style Guide
- The Best of the Best Practices (BOBP) Guide for Python
Quelques conseils supplémentaires
- « Ne réinventez pas la roue, sauf si vous souhaitez en savoir plus sur les roues » (Jeff Atwood(8)) : 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.
- Codez proprement dès le début : 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…
-
« L'optimisation prématurée est la source de tous les maux » (Donald Knuth(9)) : 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 :
- Make it work ;
- Make it right ;
- Make it fast.
- Respectez le zen du python, il vous le rendra.
VI-A-2. 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 centrale (utilisée par de nombreux programmes) favorisera la robustesse et la réutilisabilité aux dépens 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.
VI-B. 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 :
- Écrire un premier test ;
- 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é ;
- Écrire un code minimal pour passer le test ;
- Vérifier que le test passe correctement ;
- É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épendamment. 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 :
- Les tests unitaires vérifient individuellement chacune des fonctions, méthodes, etc. ;
- Les tests d'intégration évaluent les interactions entre différentes unités du programme ;
- 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égressions).
Nous avons déjà vu aux TD précédents plusieurs façons de rédiger des tests unitaires :
- Les doctest sont des exemples (assez simples) d'exécution de code inclus dans les docstring des classes ou fonctions :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
def
mean_power
(
alist, power=
1
):
"""
Retourne la racine `power` de la moyenne des éléments de `alist` à
la puissance `power`:
.. math:: \mu = (
\f
ra
c{1}
{N}\sum_{i=0}^{N-1} x_i^p)^{1/p}
`power=1` correspond à la moyenne arithmétique, `power=2` au *Root
Mean Squared*, etc.
Exemples:
>>>
mean_power
(
[1
, 2
, 3
])
2.0
>>>
mean_power
(
[1
, 2
, 3
], power=
2
)
2.160246899469287
"""
s =
0.
# Initialisation de la variable *s* comme *float*
for
val in
alist: # Boucle sur les éléments de *alist*
s +=
val **
power # *s* est augmenté de *val* puissance *power*
# *mean* = (somme valeurs / nb valeurs)**(1/power)
mean =
(
s /
len(
alist)) **
(
1
/
power) # ATTENTION aux divisions euclidiennes !
return
mean
Les doctests peuvent être exécutés de différentes façons (voir ci-dessous) :
- avec le module standard 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(10). 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. :
Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.def
test_empty_init
(
):with
pytest.raises
(
TypeError
): youki=
Animal
(
)def
test_wrong_init
(
):with
pytest.raises
(
ValueError
): youki=
Animal
(
'Youki'
,'lalala'
)def
test_init
(
): youki=
Animal
(
'Youki'
,600
)assert
youki.masse==
600
assert
youki.vivantassert
youki.estVivant
(
)assert
not
youki.empoisonneLes tests sont exécutés via py.test programme.py ;
- le module unittest de la bibliothèque standard permet à peu près la même chose que pytest, mais avec une syntaxe souvent plus lourde. unittest est étendu par le module non standard nose.
VI-C. Outils de développement▲
Je fournis ici essentiellement des liens vers des outils pouvant être utiles pour développer en Python.
VI-C-1. Integrated Development Environment▲
- idle, l'IDE intégré à Python.
- emacs + python-mode pour l'édition, et ipython pour l'exécution de code (voir Python Programming In Emacs).
- spyder.
- PythonToolkit.
- pyCharm (la version community est gratuite).
- Etc.
VI-C-2. 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 œuvre…
- pycodestyle (ex-pep8) et autopep8.
- pyflakes.
- pychecker.
- pylint.
VI-C-3. Débogage▲
Les débogueurs permettent de se « plonger » dans un code en cours d'exécution ou juste après une erreur (analyse postmortem).
- Module de la bibliothèque standard : pdb
Pour déboguer un script, il est possible de l'exécuter sous le contrôle du débogueur pdb en s'interrompant dès la 1re instruction :
python -
m pdb script.py
(
Pdb)
Commandes (très similaires à gdb) :
- h[elp] [command] : aide en ligne ;
- q[uit] : quitter ;
- r[un] [args] : exécuter le programme avec les arguments ;
- d[own]/u[p] : monter/descendre dans le stack (empilement des appels de fonction) ;
- p expression : afficher le résultat de l'expression (pp : pretty-print) ;
- l[ist] [first[,last]] : afficher le code source autour de l'instruction courante (ll : long list) ;
- n[ext]/s[tep] : exécuter l'instruction suivante (sans y entrer/en y entrant) ;
- unt[il] : continuer l'exécution jusqu'à la ligne suivante (utile pour les boucles) ;
- c[ont[inue]] : continuer l'exécution (jusqu'à la prochaine interruption ou la fin du programme) ;
- r[eturn] : continuer l'exécution jusqu'à la sortie de la fonction ;
- 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 ;
- disable/enable [bpnumber] : désactiver/réactiver tous ou un point d'arrêt ;
- cl[ear] [bpnumber] : éliminer tous ou un point d'arrêt ;
- ignore bpnumber [count] : ignorer un point d'arrêt une ou plusieurs fois ;
- condition bpnumber : ajouter une condition à un point d'arrêt ;
- commands [bpnumber] : ajouter des instructions à un point d'arrêt ;
- commandes ipython : %run monScript.py, %debug, %pdb
Si un script exécuté sous 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 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 :
- Trouver un bogue ;
- Écrire un test qui aurait dû être validé en l'absence du bogue ;
- Corriger le code jusqu'à validation du test.
Vous aurez alors finalement corrigé le bogue, et écrit un test s'assurant que ce bogue ne réapparaîtra pas inopinément.
VI-C-4. Profilage et optimisation▲
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 profilage permet de déterminer le temps passé dans chacune des sous-fonctions d'un code (ou ligne par ligne : line profiler, ou selon l'utilisation de la mémoire : 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 deTrue
àFalse
; - les instructions
assert
ne sont pas évaluées.
- la variable interne
-
timeit et
%
timeit statement sous ipython :Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.In [
1
]:def
t1
(
n): ...: l=
[] ...:for
iin
range(
n): ...: l.append
(
i**
2
) ...:return
l ...: ...:def
t2
(
n): ...:return
[ i**
2
for
iin
xrange(
n) ] ...: ...:def
t3
(
n): ...:return
N.arange
(
n)**
2
...: In [2
]:%
timeitt1
(
10000
)1000
loops, best of3
:950
µs per loop In [3
]:%
timeitt2
(
10000
)1000
loops, best of3
:599
µs per loop In [4
]:%
timeitt3
(
10000
)10000
loops, best of3
:18.1
µs per loop -
cProfile et pstats, et
%
prun statement sous ipython :Sélectionnez$ python
-
m cProfile-
o output.pstats monScript.py $ python-
m pstats output.pstats - 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 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 (JITJust In Time) du pur code Python via le compilateur LLVM, avec une optimisation selon le CPU (éventuellement le GPU) utilisé, p.ex. :
Sélectionnez1.
2.
3.
4.
5.from
numbaimport
guvectorize@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 JITJust In Time.
Lien : Performance tips
VI-C-5. 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).
Lien :
VI-C-6. Python packages▲
Comment installer/créer des modules externes :
- pip ;
- Hitchhiker's Guide to Packaging ;
- Packaging a python library ;
- Cookiecutter est un générateur de squelettes de projet via des templates (pas uniquement pour Python) ;
- cx-freeze <http://cx-freeze.readthedocs.io/>, pour générer un exécutable à partir d'un script.
VI-C-7. 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 VCSVersion Controling System particulièrement performant (p.ex. utilisé pour le développement du noyau Linux(11)). 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.
VI-C-8. 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'exécution 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 AstropyAstropy :
- Travis CI intégration continue ;
- Coveralls taux de couverture des tests unitaires ;
- 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).
VI-D. Python 2 vs. Python 3▲
Il existe de nombreux outils permettant de faciliter la transition 2.x → 3.x.
- L'interpréteur Python 2.7 dispose d'une option -3 mettant en évidence dans un code les parties qui devront être modifiées pour un passage à Python 3.
- Le script 2to3 permet également d'automatiser la conversion du code 2.x en 3.x.
-
La bibliothèque standard __future__ permet d'utiliser nativement des constructions 3.x dans un code 2.x, p.ex. :
Sélectionnez1.
2.
3.
4.from
__future__
import
print_function# Fonction print()
from
__future__
import
division# Division non euclidienne
print
(
1
/
2
)# Affichera '0.5'
- La bibliothèque non standard six fournit une couche de compatibilité 2.x-3.x, permettant de produire de façon transparente un code compatible simultanément avec les deux versions.
Liens