Journal Quelques bonnes pratiques Python pour 2019

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
22
30
mar.
2019

Sommaire

J'ai découvert/appris Python en le pratiquant au bureau à l'arche, et sans collègue à la fois expert et pédagogue. Du coup, j'ai accumulé plein de mauvaises pratiques que je tente désormais de corriger. Ce journal pour vous partager mes astuces et vous éviter les mêmes pièges :-)

Je ne suis pas encore un expert Python, alors merci de me corriger gentiment dans les commentaires ;-)

Je publie ce journal sous licence CC0 (sous domaine publique dans les pays où cela et possible) car je veux te permettre de recopier/modifier/réutiliser/republier ce contenu sans avoir à me citer comme auteur (sauf que la loi de pays comme la France t'oblige quand même à me citer).

Installer des modules Python

Le gestionnaire de paquets Python de référence s'appelle pip et au tout début, j'installais les dépendances Python manquantes avec :

sudo pip install "nom-du-module-python"    # en root

car cette commande ne semblait pas toujours fonctionner :

pip install "nom-du-module-python"

En fait la première commande ci-dessus rentre en conflit avec :

sudo apt install "autre-nom-du-meme-module-python"

Ça fout le bazar dans l'installation des paquets de sa distrib… :-(

Voilà à quoi ressemble mon installation avec `sudo pip` et `sudo apt`

En lisant pip ticket #5599 on comprend le besoin de préciser l'argument --user:

pip install --user "nom-du-module-python"

Mais il y a mieux:

python -m pip install --user "nom-du-module-python"

Et encore mieux si nos scripts ont migré sous Python-3:

python3 -m pip install --user "nom-du-module-python"

Donc, remettons bien en gras les bonne façons d'installer un module Python:

python2 -m pip install --user "nom-du-module-python"

python3 -m pip install --user "nom-du-module-python"

Mais quelle différence entre pip3 et python3 -m pip ? Les détails sur stackoverflow (en anglais). En bref, on est sûr que python3 -m pip utilise la même arborescence d'installation que le script que tu exécutes avec python3 "nom-du-script.py". C'est d'autant plus recommandé si ton $PATH est tarabiscoté, si tu as plusieurs installations de Python ou que tu utilises MS-Windows.

Mettre à jour les modules Python

Est-ce que pip permet de mettre à jour tous les modules d'un coup comme un apt upgrade ? Et bien, pas vraiment. Il faut le faire pour chaque module :

python3 -m pip install --user --upgrade "nom-du-module-python"

Attention, j'ai déjà réussi à casser pip avec cette commande :

pip install --upgrade pip

Pour tout mettre à jour, j'enregistre préalablement les versions des modules actuellement présents :

python3 -m pip list --user --format freeze > ~/requirements_avant_pip_upgrade.txt

Et une longue ligne de commande pour tout mettre à jour :

python3 -m pip list --user --format columns | awk 'FNR>2{print $1}' | while read module ; do python3 -m pip install --user --upgrade $module ; done

Visualiser les changements :

python3 -m pip list --user --format freeze > ~/requirements_apres_pip_upgrade.txt

diff -u ~/requirements_avant_pip_upgrade.txt ~/requirements_apres_pip_upgrade.txt | vi -

# quitter en tapant :q! [Entrée]

Ou avec une application graphique :

meld ~/requirements_avant_pip_upgrade.txt ~/requirements_apres_pip_upgrade.txt

En cas de problème, revenir aux versions précédentes :

python3 -m pip install --user -r ~/requirements_avant_pip_upgrade.txt

En fait, ce n'est pas recommandé de mettre à jour tous ses modules Python car cela risquerait de casser une autre application Python qui utilise la même dépendance. Cela m'est arrivé récemment avec le module redis dont la version 3 casse la comptabilité avec la version 2 ! (mais pourquoi ne pas avoir créé de nouvelles fonctions pour la nouvelle fonctionnalité ?)

Rendre un script exécutable

Pour lancer leur script Python, de nombreuses personnes font :

python mon-script.py

Avec deux petits changements, il est possible de le lancer juste comme ça :

./mon-script.py

Premier changement, dire que le script est exécutable :

chmod +x mon-script.py

Second changement, on rajoute sur LA PREMIÈRE ligne du fichier mon-script.py :

#!/1usr/bin/python

Ou mieux :

#!/usr/bin/python3

Ou encore bien mieux :

#!/usr/bin/env python3

Le #! s'appelle shebang et même des langages dont le # n'est pas utilisé pour les commentaires le prennent en charge, comme pour PHP.

Compatibilité Python-2 et Python-3

Les distributions GNU/Linux sont enfin en train de migrer tous leurs scripts vers Python-3 (Pyton-3.0 a 10 ans). Mais difficile d'identifier si un script est compatible seulement v2 ou seulement v3 ou s'il est compatible avec les deux.

Pour aider les intégrateurs, merci de remplacer python par python2 ou python3 dans vos projets open-source. Et si compatible avec les deux versions de Python ? Si ton application compatible v2 et v3 pourrait être utilisée sur une ancienne distribution GNU/Linux qui n'a pas Python3, alors continue d'utiliser python. Si ce n'est pas le cas, utilise alors python3.

Pour s'assurer que tes scripts Python2 soient compatibles Python3 rajoute au début de tous tes fichiers *.py :

from __future__ import absolute_import, division, unicode_literals, print_function

Attention, je me suis fait avoir avec un bug uniquement reproductible avec la ligne précédente. Quand je lançais python2 en mode interactif pour vérifier mon bout de code, je n'avais aucun soucis. Donc pense à taper from __future__ import absolute_import, division, unicode_literals, print_function quand tu veux reproduire un bug dans ton script python2. Il y a moyen de pré-exécuter cette ligne automatiquement. Avec ipython :

$ cat ~/.ipython/profile_default/startup/ipython_config.py

from __future__ import absolute_import, division, unicode_literals, print_function

Arborescence d'un projet Python

Kenneth Reitz recommandait https://github.com/kennethreitz/samplemod

docs\
sample\
tests\
LICENSE
MANIFEST.in
Makefile
README.rst
requirements.txt
setup.py

Plus récemment, l'équipe Python Packaging Autority recommande https://github.com/pypa/sampleproject

data\
sample\
tests\
LICENSE.txt
MANIFEST.in
README.md    # PyPi accepte Markdown depuis 2018
setup.cfg    # Alternative à setup.py
setup.py
tox.ini      # Plus besoin de requirements-test.txt

Si tu préfères Markdown à reStructuredText, sache que PyPi gère même les tableau Markdown.

Mais une révolution est en cours avec les PEP-517 et PEP-518, avec l'adoption du fichier pyproject.toml en alternative du vénérable setup.py et son setup.cfg (même pip adopte pyproject.toml). Mais la vrai révolution est le découplage entre empaquetage et installation. Pour les anglophones je recommande la lecture de Python packaging - Past, Present, Future par Bernát Gábor.

Donc bientôt nous pourrons avoir une arborescence plus simple :

docs\
sample\
tests\
LICENSE.txt
README.md
pyproject.toml
tox.ini

Par exemple, les dépôts de code source de poetry et de la PEP-517.

Et tes astuces ?

Merci de partager tes recommandations, tes mésaventures, tes bonne pratiques… :-D

  • # Installation de paquets de développement

    Posté par  (site web personnel) . Évalué à 5.

    On peut avoir au moins deux raisons bien différentes d'installer des paquets Python : pour les utiliser comme des outils classiques, ou parce qu'ils sont nécessaires dans un projet en développement.
    Dans le premier cas, j'installe avec --user. Dans le second, il me semble assez impératif de créer un virtualenv (ou équivalent, en gros c'est un chroot pour Python) qui permet d'isoler ce projet du reste du système et d'y mettre les bonnes versions.

    Sinon, deux sites à connaître, le premier est Sam & Max (attention, lien NSFW mais avec de bons conseils et des retours d'expérience intéressants), le second est The Hitchhiker's Guide to Python.

    • [^] # Re: Installation de paquets de développement

      Posté par  . Évalué à 3.

      Personnellement, j'utilise pipenv pour la gestion des virtualenv par projet. L'outil est bien fait, ne nécessite pas de configuration particulière de son shell et permet de gérer finement les dépendances en prod ou en dev.

    • [^] # Re: Installation de paquets de développement

      Posté par  (site web personnel) . Évalué à 6.

      pip et virtualenv, ça devrait toujours aller ensemble. Histoire d'éviter les soucis de mises à jour pyOpenssl en conflit avec la version fournie par le gestionnaire de paquets de la distrib' qui veut passer une mise à jour de sécurité et qui tombe sur la version fournie par pip qui a écrasé en douce la version dudit gestionnaire de paquets, par exemple… Pareil dans ansible, le module pip devrait toujours utiliser le paramètre virtualenv (et ça vaut le coup d'avoir une règle ansible-lint pour l'imposer).

  • # vimdiff

    Posté par  . Évalué à 4.

    Ça vaut ce que ça vaut, mais…

    diff -u ~/requirements_avant_pip_upgrade.txt ~/requirements_apres_pip_upgrade.txt | vi -

    Plutôt que de faire un pipe dans le stdin de vi, il me semble plus efficace de demander à vimdiff de faire directement le diff lui même :

    vimdiff ~/requirements_avant_pip_upgrade.txt ~/requirements_apres_pip_upgrade.txt
    Puis tant qu'à faire, quitte à utiliser vimdiff, autant ne économiser une commande et économiser la création du deuxième fichier :

    vimdiff ~/requirements_avant_pip_upgrade.txt <(python3 -m pip list --user --format freeze)

  • # pip

    Posté par  . Évalué à 5.

    pip s'est enrichit au fil du temps de quelques options supplémentaires bien pratiques, notamment pour voir les paquets qui peuvent être mis à jour:

    pip list --outdated
    

    A combiner si besoin avec --user ou --local (pour les virtualenv ayant accès au site-packages global). Et aussi --editable pour les paquets installés en mode develop (avec pip install -e).

    Il manque essentiellement une commande pour tout mettre à jour, du coup j'utitlise ça:

    pip list --outdated --format=freeze --exclude-editable | \
        cut -d = -f 1 | xargs -n1 pip install -U
    

    mais comme il n'y a pas de vérification des versions requises par les dépendances, il faut parfois ajuster manuellement et forcer les versions de certains paquets.

  • # Xkcd

    Posté par  . Évalué à 10.

    Le xkcd de référence : xkcd python environnement

    Et mes machines ressemblent un peu à ça. Pourquoi personne ne centralise les gestionnaires de paquets ?

    Systemd --pack upgrade
    doit devenir la solution.

    (Ami lecteur un troll s'est glissé dans ce message, saura tu le reconnaître ?)

    • [^] # Re: Xkcd

      Posté par  (site web personnel, Mastodon) . Évalué à 3.

      Pourquoi personne ne centralise les gestionnaires de paquets ?

      GNU essaye précisément de faire ça avec leur gestionnaire de paquet Guix. Voilà la section de la documentation qui explique ça : https://www.gnu.org/software/guix/manual/en/html_node/Invoking-guix-import.html

      Ils ont déjà un mécanisme d’import pour les paquets Python, Ruby, R, Perl, et quelques autres.

      • [^] # Re: Xkcd

        Posté par  (site web personnel) . Évalué à 3.

        Et nix fait déjà ça et gère les packets python.

        λ paddle ~ → python
        The program ‘python’ is currently not installed...
        
        λ paddle ~ → nix-shell -p 'python3.withPackages(p : [])'
        
        [nix-shell:~]$ python
        Python 3.7.2 (default, Dec 24 2018, 03:41:55) 
        [GCC 7.4.0] on linux
        Type "help", "copyright", "credits" or "license" for more information.
        >>> import numpy
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        ModuleNotFoundError: No module named 'numpy'
        >>> 
        KeyboardInterrupt
        >>> 
        
        [nix-shell:~]$ ^C
        
        [nix-shell:~]$ exit
        λ paddle ~ → nix-shell -p 'python3.withPackages(p : [p.numpy])'
        [nix-shell:~]$ python
        Python 3.7.2 (default, Dec 24 2018, 03:41:55) 
        [GCC 7.4.0] on linux
        Type "help", "copyright", "credits" or "license" for more information.
        >>> import numpy
        >>> 
        KeyboardInterrupt
        >>> 
        
        [nix-shell:~]$ ^C
        
        [nix-shell:~]$ exit
        λ paddle ~ → nix-shell -p 'python2.withPackages(p : [p.numpy])'
        these derivations will be built:
          /nix/store/4r2lhzcnblj58pbalpw6s0j0q34mrdc3-python-2.7.15-env.drv
        building '/nix/store/4r2lhzcnblj58pbalpw6s0j0q34mrdc3-python-2.7.15-env.drv'...
        created 686 symlinks in user environment
        
        [nix-shell:~]$ python
        Python 2.7.15 (default, Apr 29 2018, 23:18:59) 
        [GCC 7.4.0] on linux2
        Type "help", "copyright", "credits" or "license" for more information.
        >>> import numpy
        >>> 
        KeyboardInterrupt
        >>> 
        
  • # Attention à pip hors des environnements virtuels

    Posté par  (site web personnel) . Évalué à 4.

    Je passe sur le pip qui va bidouiller dans les répertoires du système, à prohiber.

    Mais le pip qui installe des librairies --user est aussi risqué. Ces librairies peuvent masquer celles installées au niveau du système… et faire que certains logiciels écrits en Python ne fonctionnent plus. C'est au moins à savoir, pour pouvoir diagnostiquer, et ça se répare facilement (on supprime de l'installation de la librairie pour l'utilisateur).

    Votez les 30 juin et 7 juillet, en connaissance de cause. http://www.pointal.net/VotesDeputesRN

  • # Quelques bonnes pratiques Python pour 2019

    Posté par  . Évalué à 10.

    J'en ai au moins une moi : arretez d'utiliser Python 2 (bordel !).

  • # Autres astuces non mentionnées dans mon journal

    Posté par  (site web personnel) . Évalué à 5.

    Quelques autres astuces intéressantes dans l'écosystème Python que j'ai oublié de mentionner dans mon journal.

    Règles du code source

    Bien que de nombreux développeurs sont d'accords pour respecter la PEP-8, on a tous nos préférences pour telle ou telle nuance. Par exemple, personnellement j'apprécie lire des valeurs alignées comme ceci :

    capitaine = { 'age':    42,
                  'nom':    'Grant',
                  'bateau': 'Britannia' }

    Mais ce n'est pas l'avis de tous et chacun a de bons arguments. Par conséquent, du temps précieux est consacré à convaincre/négocier sur tel ou tel aspect. Et puis, avec la rotation naturelle de l'équipe (turn-over), les décisions du passé ne conviennent pas toujours à la nouvelle équipe ! La preuve que l'on y passe du temps, mon bout de code ci-dessus va être repris dans un commentaire plus bas.

    Pour mettre tout le monde d'accord, économiser son temps et éviter les polémiques/frustrations, utilisons black. C'est un script python tout-en-un de 4000 lignes qui décide/réécrit tout seul vos fichiers Python. L'absence de fichier de configuration évite tout marchandage. Son objectif est de réduire le diff entre deux commits (minimiser le nombre de lignes modifiés).

    Mon code ci-dessus devrait être converti en : (pas testé)

    capitaine = {
        "age": 42,
        "nom": "Grant",
        "bateau": "Britannia",
    }

    Tests unitaires

    Historiquement, nous avons unittest. D'autres alternatives intéressantes : nose et pytest. Ce dernier permet d'écrire très simplement ses tests unitaires sans avoir à implémenter une classe :

    def incremente(x):
        return x + 1
    
    def test_incremente():
        assert incremente(3) == 4

    Analyse statique de code

    Quelques outils intéressants :

    • mypy
    • pylint
    • flake8

    Annotation de type

    Comme Spack nous le signalait en 2014 et récemment amélioré par Python 3.7, nous pouvons aider l'analyse statique du code source Python en spécifiant les types attendus :

    import typing
    
    def ajoute(valeur: int, lst: typing.Sequence[int]) -> typing.List[int]:
        ret = []
        for elem in lst:
            ret.append(elem + valeur)
        return ret

    Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

    • [^] # Re: Autres astuces non mentionnées dans mon journal

      Posté par  (site web personnel) . Évalué à 3.

      Je suis tout à fait d'accord pour black.

      L'aspect qui en résulte ne me plaît pas forcément, mais c'est une affaire de goûts. Comme il y a de vraies raisons objectives pour l'utiliser, je m'y suis mis également.

      Je m'impose (mais sans outil) de trier dès que possible par ordre alphabétique (par exemple l'ordre d'écriture dans un set, l'ordre des arguments par mot-clef, …). Ça permet de limiter également des diff d'une part, et de simplifier la recherche visuelle.

      • [^] # Re: Autres astuces non mentionnées dans mon journal

        Posté par  (site web personnel) . Évalué à 2.

        Au boulot on utilise yapf, mais je n'étais pas emballé par la config utilisée. J'ai essayé black mais je ne suis vraiment pas convaincu par le formatage. Il ne va pas changer que l'indentation et les retours chariots, mais aussi le type de guillemets utilisé (guillemets doubles partout). C'est bien pour l'homogénéité, mais cela altère plus que juste le formatage. Perso je préfère des lignes courtes avec un paramètre par ligne car cela permet d'avoir des diff minimalistes et faciles à lire, et d'éviter au maximum les conflits. Black va lui au contraire essayer de tout faire tenir sur une ligne.

        De son côté, yapf permet de donner des indications sur le formatage, mais c'est toi qui les contrôles.
        Par exemple, prendre l'habitude de mettre une virgule après le dernier paramètre d'une liste (de valeurs ou d'arguments) permet aussi de réduire les diff. Ainsi quand on ajoute un nouveau paramètre, le pas besoin de modifier la ligne précédente pour rajouter une virgule. On a un diff propre avec juste une ligne ajoutée correspondant à la valeur ajoutée. Et yapf a la bonne idée de comprendre qu'une virgule sans autre valeur derrière indique que tu préfères un formatage avec un paramètre par ligne.

        On est finalement restés sur yapf avec quelques améliorations sur la config.

  • # Python Build Reasonableness

    Posté par  (site web personnel) . Évalué à 4. Dernière modification le 03 avril 2019 à 05:32.

    Pour éviter de passer du temps à réaliser des tâches répétitives à chaque version, l'équipe de OpenStack les a automatisées dans un script qui est devenu en 2010, le module pbr pour Python Build Reasonableness (avec sagesse). Ce module a énormément évolué en s'adaptant aux évolutions de Python sur dix ans (150 contributeurs).

    Installation

    sudo apt install python3-pbr        # v4.2.0 (Ubuntu 18.10)
    
    # ou une version bien plus récente:
    
    python3 -m pip install --user pbr   # v5.1.3

    Fonctionnalités

    • Récupère automatiquement la version à partir du tag Git et la passe à setuptools (setup.py) ;
    • Réplique les dépendances du fichier requirements.txt vers setuptools (install_requires) ;
    • Maintient la liste des AUTHORS à partir du git log ;
    • Génère le ChangeLog en cherchant les mots feature, api-break, deprecation et bugfix dans le git log ;
    • Délègue à reno la gestion des Release Notes ;
    • Gère le fichier MANIFEST.in ;
    • Lance les tests via tox (déprécié) ;
    • Lance sphinx pour produire la documentation.

    Avant

    ├── nom_du_module_python/
    ├── AUTHORS
    ├── CHANGES
    ├── LICENSE
    ├── MANIFEST.in
    ├── README.md
    ├── RELEASENOTES.txt
    ├── requirements.txt  # dependance_1 dependance_2...
    └── setup.py
        ║
        ╚═╡ from setuptools import setup, find_packages
          │ from os import path
          │
          │ setup(name = "nom_du_module_python",
          │     version = "1.0.0",
          │     author = "Michel Martin",
          │     author_email = "m.martin@example.com",
          │     description = "Une courte description",
          │     long_description = open(path.join(path.dirname(__file__), 'README.md'), encoding='utf-8').read()long_description_content_type = "text/markdown",
          │     license = "AGPL",
          │     packages = find_packages(exclude=["test.*"]),
          │     install_requires = ["dependance_1", "dependance_2", "dependance_3", "dependance_4"],
          │ )

    Après

    ├── nom_du_module_python/
    ├── LICENSE
    ├── README.md
    ├── requirements.txt
    ├── setup.cfg
    │   ║
    │   ╚═╡ [metadata]
    │     │ name = nom_du_module_python
    │     │ description = Une courte description
    │     │ description-file = README.md
    │     │ author = Michel Martin
    │     │ author-email = m.martin@example.com
    │     │ license = AGPL
    │
    └── setup.py
        ║
        ╚═╡ from setuptools import setup
          │
          │ setup(setup_requires = ["pbr"],
          │     pbr = True,
          │ )

    Bon, c'est vrai, depuis setuptools-30.3.0 (déc. 2016) on peut remplacer setup.py par setup.cfg. Cependant, le Python Packaging Authority recommande d'utiliser principalement le setup.py. Néanmoins, des projets comme tox utilisent principalement setup.cfg.

    Distinction entre install_requires et requirements.txt

    Le module pbr permet d'éviter de gérer en double la liste des dépendances : pbr spécifie le paramètre install_requires à partir du fichier requirements.txt. Mais, sémantiquement, ces deux listes ont deux objectifs différents :

    • Le paramètre install_requires du fichier setup.py (ou setup.cfg) est inséré dans le livrable et ne précise pas toujours la version des dépendances, du moins l'intervalle des versions pour lequel le livrable est censé être compatible ;
    • Le fichier requirements.txt est fourni à pip pour installer des versions précises pour lesquelles l'application a été validée.

    De plus, requirements.txt peut contenir des arguments de la ligne de commande pip comme --index-url https://pypi.python.org/simple/ afin d'indiquer explicitement à partir de quel dépôt télécharger les dépendances. Les développeurs utilisent souvent ce fichier pour avoir les mêmes dépendances et éviter de perdre du temps avec une incompatibilité absconse.

    En résumé, install_requires définit les dépendances compatibles (pour la livraison) et requirements.txt spécifie les versions validées (pour le déploiement). Pour plus de détail sur cette sémantique, lire le coup de gueule de Donald Stufft (2013, en anglais). Lire aussi la documentation officielle (en anglais).

    Cependant, dans la pratique, ces deux listes sont souvent gérées de la même façon, donc autant mutualiser les efforts.   :-)

    Futur

    Actuellement, pbr n'est pas encore compatible avec le fichier pyproject.toml (PEP-517 et PEP-518) et ne prend pas en charge les alternatives à setuptools comme flit et poetry (découplage entre empaquetage et installation). Mais les développements sont prévus d'après la description du module :

    As Metadata 2.0 and other modern Python packaging PEPs come out, PBR aims to support them as quickly as possible.

    Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.