8. Développer en Python🔗

8.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 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 !

8.1.1. 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)

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.

Voir également:

8.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 de bas niveau, utilisée par de nombreux programmes (p.ex. 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.

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    def mean_power(alist, power=1):
        r"""
        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
        """
    
        # *mean* = (somme valeurs**power / nb valeurs)**(1/power)
        mean = (sum( val ** power for val in alist ) / len(alist)) ** (1 / power)
    
        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
    def test_empty_init():
        with pytest.raises(TypeError):
            Animal()
    
    
    def test_wrong_init():
        with pytest.raises(ValueError):
            Animal('Youki', 'lalala')
    
    
    def test_init():
        youki = Animal('Youki', 600)
        assert youki.masse == 600
        assert youki.vivant
        assert not youki.empoisonne
    

    Les 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.

8.3. Outils de développement🔗

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

8.3.1. Integrated Development Environment🔗

8.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…

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

  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.

8.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 évaluées.

  • 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 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)
    
  • cProfile et pstats, et %prun statement sous 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(<module>)
     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 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 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 JIT.

Lien:

Performance tips

8.3.5. Documentation🔗

Lien:

Documentation Tools

8.3.6. Python packages🔗

Comment installer/créer des modules externes:

8.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 3). 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:

8.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, non maintenu).

Notes de bas de page

1

Dont seulement 19 ont été écrits.

2

pytest ne fait pas partie de la bibliothèque standard. Il vous faudra donc l’installer indépendemment si vous voulez l’utiliser.

3

Et maintenant du code Windows!