6. Développer en python

6.1. Le zen du python

Le zen du Python (PEP 20) est une série de 20 aphorismes [1] 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 l’aspect pratique à la pureté.
  10. Ne jamais passer les erreurs sous silence,
  11. ou les faire taire explicitement.
  12. Face à l’ambiguïté, ne pas se laisser tenter à deviner.
  13. Il doit y avoir une – et si possible 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 mieux qu’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 nommage sont une sacrée bonne idée, utilisons-les plus souvent !

6.1.1. Us et coutumes

Quelques conseils supplémentaires:

  • « Ne réinventez pas la roue, sauf si vous souhaitez en savoir plus sur les roues » (Jeff Atwood [3]): 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 [4]): 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.

6.1.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é au dépend 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.

6.2. 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 bugs 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 test, 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çon 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:

     1
     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 = (\frac{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 [2]. 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.:

     1
     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.vivant
        assert youki.estVivant()
        assert not youki.empoisonne
    

    Les tests sont exécutés via py.test programme.py.

  • Le module unittest de la librairie 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.

6.3. Outils de développement

Je fournis ici essentiellement des liens vers des outils pouvant être utiles pour développer en python.

6.3.1. Integrated Development Environment

6.3.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 oeuvre...

6.3.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 post-mortem).

  • 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 1ère 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:

  1. trouver un bug
  2. écrire un test qui aurait du être validé en l’absence du bug
  3. corriger le code jusqu’à validation du test

Vous aurez alors au final corrigé le bug, et écrit un test s’assurant que ce bug ne réapparaîtra pas inopinément.

6.3.4. Profilage et optimisation

Avertissement

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 de True à False,
    • les instructions assert ne sont pas exécutés.
  • timeit et %timeit statement sous 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 xrange(n) ]
    ...:
    ...: def t3(n):
    ...:     return N.arange(n)**2
    ...:
    In [2]: %timeit t1(10000)
    1000 loops, best of 3: 950 µs per loop
    In [3]: %timeit t2(10000)
    1000 loops, best of 3: 599 µs per loop
    In [4]: %timeit t3(10000)
    10000 loops, best of 3: 18.1 µs per loop
    
  • cProfile & pstats, et %prun statement sous ipython:

    $ 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 (JIT) du pur code python via le compilateur LLVM, avec une optimisation selon le CPU (éventuellement le GPU) utilisé, p.ex.:

    from numba import 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 JIT.

Lien: Performance tips

6.3.5. Documentation

Lien:

Documentation Tools

6.3.6. Python packages

Comment installer/créer des modules externes:

6.3.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 VCS particulièrement performant (p.ex. utilisé pour le développement du noyau Linux [5]). 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:

6.3.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’é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
  • 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)

6.4. 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.:

    from __future__ import print_function  # Fonction print()
    from __future__ import division        # Division non-euclidienne
    
    print(1/2)                             # Affichera '0.5'
    
  • La librairie 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 2 versions.

Liens

Notes de bas de page

[1]Dont seulement 19 ont été écrits.
[2]pytest ne fait pas partie de la librairie standard. Il vous faudra donc l’installer indépendemment si vous voulez l’utiliser.
[3]« Don’t reinvent the wheel, unless you plan on learning more about wheels » – Jeff Atwood
[4]« Premature optimization is the root of all evil » – Donald Knuth
[5]Et maintenant du code Windows!