Un projet Python à l’épreuve de la qualité

Emmanuelle R. et Gaëtan L.

 

21/01/2020

  • Introduction
  • 1- Finalités de la qualité
  • 2- Le pythonique
  • 3- Méthodes et orientations en programmation
  • 4- Moyens pour y parvenir : au fond
  • 5- Moyens pour y parvenir : dans la forme
  • 6- Quand ça bloque
  • 7- Propositions d'outils pour (bien) développer
  • Quelques sources et ressources

Sommaire

"La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il n'y a plus rien à retirer.

Saint Exupery

 

 

 

 

Introduction

  • en cas d’oubli du développeur actuel
  • communication dans l'équipe.

Pourquoi ce sujet ?

  • entre les réalisateurs d'un programme
  • et entre un programme et son utilisateur

L'idée de "sport collectif", de discipline :

La qualité est là pour établir une règle du jeu :

"Code is read much more often than it is written"

dixit Guido van Rossum (le créateur de Python)

D'un côté,

il est important d'apprendre à respecter la règle du jeu,

càd des normes de développement

dans les méthodes, au fond, dans la forme.

 

D'un autre côté,

Python, par son mode communautaire, entretient un rapport spécifique à la qualité : le pythonique.

 

La simplicité est un principe à la base de nombreuses règles de qualité en général, et en particulier avec Python.

 

Cf. "Less is more" et van der Rohe : il fut directeur du Bauhaus

or en informatique aussi on parle de design, d'architecture…

Mais la simplicité n'est pas un garant suffisant, il faut entre autre de la cohérence, de la lisibilité par exemple, pour que la qualité atteigne ses objectifs.

a=[0]*3

# nom de variable ésotérique
# espacement non respectés
# objet délicat à  manipuler
# que penser de cette proposition ?
a = 4
z = 5
def az(a):  # MOCHE ET BUGUÉ :
	a+=z
az(z)
print(a)

4


# et de celle-ci ? :-)
nombre = 4
augm = 5
def incrementer_valeur(valeur):  # ?
	"""modifies the parameter $valeur and returns it"""
	return valeur + augm
nombre = incrementer_valeur(nombre)
print(nombre)

9

1- Finalités de la qualité

2- Le pythonique

3- Méthodes et orientations en programmation

4- Moyens pour y parvenir : au fond

5- Moyens pour y parvenir : dans la forme

6- Quand ça bloque

7- Propositions d'outils pour (bien) développer

Rappel des 7 axes que nous allons suivre

1- Finalités de la qualité

Les finalités de la qualité d'un programme :

 

  • produire un résultat prévisible, toujours identique à paramètres identiques, et qui anticipe toutes les combinaisons des paramètres
  • pouvoir être repris sans peine après une phase d'arrêt dans son développement
  • pouvoir évoluer en fonction de spécifications nouvelles

 

Ces finalités  mettent en relation des lignes de code avec un contexte : un client, un utilisateur, d'autres programmeurs.

 

Le programmeur semble seul face à son code - et il en tire parfois une certaine jouissance. En fait, ce qu'il produit est un objet destiné à entrer dans un réseau de relations, et il doit en tenir compte ! dans une perspective temporelle : la dette technique doit être minimisée

Côté code, la qualité est un impératif qui pèse dans le succès - et le coût potentiel - d'une application.

2- Le pythonique

« Pythonique » ?

 

Code Python bien conçu, un code idiomatique (en accord avec les règles d’usage du langage, et donc compréhensible par tout développeur).

 

  • Zen of Python
  • KISS
  • Les mécanismes du language
  • Biblioteque standard

PEP20 : le Zen de Python                                              

 

  • Le beau est préférable au laid.
  • L’explicite est préférable à l’implicite.
  • Le simple est préférable au complexe.
  • Le complexe est préférable au compliqué.
  • Le plat est préférable à l’imbriqué.
  • L’aéré est préférable au dense.
  • La lisibilité compte.
  • Les erreurs ne devraient jamais se produire silencieusement.
  • À moins d’être explicitement tues.
  • En cas d’ambiguïté, résister à la tentation de deviner.
  • Il devrait y avoir une – et de préférence une seule – manière évidente de le faire.
  • Maintenant est préférable à jamais.
  • Bien que jamais soit souvent préférable à tout de suite.
  • Si l’implémentation est difficile à expliquer, c’est une mauvaise idée.
  • Les espaces de noms sont une sacrée bonne idée – utilisons-les plus souvent !
  • (Et la vingtième)

20 règles Ecrite par Tim Peters. Celle-ci énonce les règles suivant un poème. On peut la retrouver via l’instruction import this dans un interpréteur Python.

Explicit is better than implicit.

L’explicite est préférable à l’implicite.

 

Lire un code Python sans se demander sans cesse ce que fait telle ou telle ligne. Utiliser des noms et des constructions explicites.

Par exemple, en programmation objet, lors d’un héritage et de la surcharge de la méthode d’initialisation (__init__), il convient d’appeler explicitement la méthode de la classe parente. Cela ne sera jamais fait automatiquement dans le dos du développeur, afin d’avoir la main sur le comportement voulu.

class User():
    def __init__(self, name):
        self.name = name

class SecureUser(User):
    def __init__(self, name, password):
        super().__init__(name)
        self.password = password

Flat is better than nested.

Le plat est préférable à l’imbriqué.

 

Quand on lit un code, il est facile de perdre le fil et d’oublier à quel endroit on se trouve si de nombreux niveaux d’imbrications se succèdent.

Préférer donc d'éviter les imbrications inutile (code plat).

==> retourner directement quand des préconditions ne sont pas validées, plutôt que de placer le contenu de notre fonction dans plusieurs sous-niveaux de conditions.

# par ex., ne pas faire
def print_items():
    if True:
        print('etc')
        print('etc')
        print('etc')
        print('etc')
        print('etc')
    else:
        return
# mais faire

def print_item():
    if False:
        return
    print('etc')
    print('etc')
    print('etc')
    print('etc')
    print('etc')

There should be one – and preferably only one – obvious way to do it.

Il devrait y avoir une – et de préférence une seule – manière évidente de le faire.

 

Python prône le fait qu’il existe toujours une manière optimale de procéder, et donc que toutes ne se valent pas. Celle-ci est préférable car évidente.

La manière évidente d’itérer sur des nombres est par exemple d’utiliser une boucle for.

for i in range(100):
    print(i)

Keep it simple, stupid (KISS)

Il est inutile de créer de nouvelles classes trop vite.

Par exemple, pour un objet qui ne contiendrait que des données, associées à aucune méthode, un dictionnaire suffit:

user = {'username': 'Giles', 'realname': 'Giles Dupont', 'password': '12345'}

= Regrouper dans des fonctions, une action en une seule.

Chaque fonctions doit avoir une action a la fois, et c’est l'enchaînement des fonctions qui produit la fonctionnalité finale.

You ain’t gonna need it (YAGNI)

Tu n’en auras pas besoin

Code composable:

Les mécanismes du langage:

L'unpacking (ou deconstruction)

a = 5
b = 2
a, b = b, a
print(a, b)
2 5

Technique qui permet l’assignation de plusieurs variables en une seule instruction.

Ce qui se passe en interne lors de la 3ème ligne est la création d’un tuple (b, a), qui est ensuite déconstruit et son contenu stocké dans les variables a et b.

 

Conditions

L'unpacking (ou deconstruction)

Toute valeur en Python peut s’évaluer sous forme d’un booléen, sans conversion.

  • Les valeurs None, 0 et le conteneurs vides donnent False
  • Les autres nombres, les conteneurs non vides, valeur non fausse donnent à True.
if ma_liste:
    print("non vide")

La bibliothèque standard

Les fonctions Built-in:

Voir la liste exhaustive sur : Built-in Functions — Python 3.8.1 documentation

ex: min(), max(), sum(), sorted(), etc.

 

print('{} + {} = {}'.format(2, 3, 2 + 3))

.format():

.zip()

x = [1, 2, 3]
y = [4, 5, 6]
zipped = zip(x, y)

Un exemple :

  • Le bloc with open() as marque implicitement la fermeture du fichier, pas de risque d'oublier son close()
  • Utiliser les fonctions max() et min() plutot que de les déterminer à l'ancienne
  • la fonc enumerate() renvoit à la fois le numéro de la ligne (à partir de 0) et la ligne elle même, plus besoin de tenir un compteur à jour
  • ne pas tenir manuellement un compteur d'itération pour connaitre notre position dans une liste, mais lui préférer l'utilisation d'un générateur.
with open(pythonique, 'r') as f:
      
      for i in enumerate(f):
         longueur = abc.split(',')
       
      for i in mylist:
      # et non : for i in len(mylist) /
      # + increment

3- Méthodes et orientations en programmation

 

- différents paradigmes de programmation

 

- l'agilité

 

- les tests

 

- un sens des responsabilités en Python

 

- rares sont les règles absolues de qualité

 

Différents paradigmes de programmation ont un impact

sur la qualité du code :

  • la programmation orienté objet. Comme on l'a vu avec le Pythonique, il faut utiliser les méthodes des objets que l'on manipule. La possibilité de créer des instances offre un cadre régulier de programmation censément facile à appréhender par des programmeurs nouveaux venus sur le projet : méthodes, espace de nom
    Cela  ne revient pas à répudier la programmation procédurale, qui reste adaptée à des cas tels que par ex., un programme court à usage unique et circonstancié.
  • la programmation fonctionnelle : issue de travaux mathématiques (d'où par ailleurs les fonctions lambda). On a évoqué dans la partie précédente le code composable (5.6). La programmation fonctionnelle intègre ce principe et a l'avantage de poser un cadre où la valeur des variables est sous contrôle. Les fonctions ne modifient pas les variables, mais renvoient une valeur modifiée. Cela renforce la traçabilité des variables et évite les mauvaises surprises (les "effets de bord"). Pour cela, l'idéal est une fonction n'acceptant qu'une valeur en entrée (le paramètre), et ne retournant que la nouvelle valeur. Déjà vu avec les décorateurs : le paramètre peut être une fonction.

POO : tout le monde est d'accord sur les méthodes

et sur les propriétés des objets : en Python, tout est objet

On l'a vu en introduction, parallèle possible avec le design, qui implique aussi un collectif.

=> en programmation : les design patterns

 

Programmation fonctionnelle : tout le monde est d'accord sur les variables, standardisation de la forme des fonctions

 

Dans un cadre professionnel, des méthodes renforcent aussi la qualité des spécifications, des livrables : l'agilité par exemple.

Pas d'assez d'expérience en ce domaine pour l'évoquer plus, et cela nous écarterait du sujet de l'approfondir alors que nous l'aborderons prochainement. Rappelons justes deux des maximes "professionnelles" de la PEP 20  : "Maintenant est préférable à jamais.",  "Bien que jamais soit souvent préférable à tout de suite."
Eviter l'urgence ne signifie pas procrastiner => maintenant > jamais > tout de suite

L'encadrement formel de la production de code par
les tests

 

les tests unitaires (qui s'appliquent à une partie précise d'une application)

 

le TDD par ex. : test driven development

le développement guidé par les tests :

le fait que l'écriture du code soit soumise aux contraintes du test produit un code qui répond strictement aux besoins, qui est plus concis, qui évite les régressions…

 

Là aussi, temps et expérience manquent encore

pour en parler plus et mieux

mais les tests sont intrinsèquement

partie prenante de la qualité

Un sens des responsabilités en Python

 

Autres principes qui participent du pythonique dans le sens d'une morale de la responsabilité. Postulat : j'assume donc je fais de la qualité.

 

We’re all consenting adults here - Nous sommes ici entre adultes consentants

Exemple : on accède à un attribut préfixé par _ (donc signalé comme protégé) en connaissance de cause : rien ne nous en empêche.

NB : l'accès direct aux attributs est préférable en Python aux méthodes getter/setter.

 

Easier to ask forgiveness than permission (EAFP) - Il est plus facile de demander pardon que la permission.

Tenter une instruction et gérer les erreurs au fur et à mesure, plutôt que de vérifier à l'avance une condition susceptible d'avoir évolué le moment venu (ex. : lecture de fichier).

Rares sont les règles absolues de qualité

 

Première règle qui n'est pas respectée par Tim Peters : celle de la vingtième règle.

Implicite : elle peut être vue comme une ligne vide, donc une incitation à aérer le code.

 

Cette absence peut être une illustration du principe de relativité "Practicability beats purity" : une incitation à ne pas devenir intégriste dans l'application des règles (surtout formelles). Par exemple : les 4 espaces d'indentation de la PEP 8 peuvent dans certains cas (justifiés (lesquels ?)) laisser place à une tabulation de même largeur. Mieux vaut aussi être cohérent avec un module qui n'appliquerait pas strictement les règles, en s'adaptant à lui.

 

Compter ainsi avec le style, les compétences de chaque développeur - et respecter

aussi un autre principe

fondamental, la sécurité.

def coherence(securite):
    if not securite:
        return "wtf"
    competences = dict(zip(principes, pragmatisme))
    return "qualite"

4- Moyens pour y parvenir : au fond

 

- import this ?

 

- exploiter les ressources propres au Python

 

- recourir aux modules

 

- les fonctions dans le fond

 

- du côté des variables

 

- conventions de nommage des variables dans le fond

 

Il devrait y avoir une – et de préférence une seule – manière évidente de le faire.

Si l’implémentation est difficile à expliquer, c’est une mauvaise idée.

Si l’implémentation est facile à expliquer, il peut s’agir d’une bonne idée.

Les espaces de noms sont une sacrée bonne idée – utilisons-les plus souvent !

import this ?

 

Que dit Tim Peters dans le Zen de Python concernant le fond ?

Exploiter les ressources propres au Python

 

Une illustration : l'exemple à la fin de la partie Pythonique

 

Connaissance de la bibliothèque standard, et toujours de ses fonctions

 

La fonction help() pour avoir plus d'information sur un objet

sur une fonction, un type, un module

et la fonction dir() pour lister les méthodes d'un objet

 

Des mécanismes propres au Python :

Déjà vu : unpacking (a, b = b, a), conditions ([] == False), gestionnaire de contexte with

Mais aussi : listes de compréhension [expr for x in foo], décorateurs @deco; def foo():

=> à voir, les décorateurs de la bib. standard : staticmethod, classmethod, property

Recourir aux modules

Extraits de l'article Les secrets d'un code pythonique
les illustrations en moins, mais lien à la fin :-)

"Le module collections comporte d’autres structures de données essentielles au langage : OrderedDict, namedtuple, Counter, ou encore defaultdict qui sera préférable à une utilisation systématique de setdefault. Des développeurs débutants auront le réflexe de recréer ces classes, alors qu’elles sont à portée de main."

"Viennent ensuite les autres modules, tels que itertools, functools ou operator. Ces modules regroupent divers utilitaires sympathiques, qui simplifient grandement le code. En faire bon usage permet de se conformer aux standards du langage."

"Enfin, suivant le domaine d’application du projet, entrent en compte les modules dédiés : re, math, random, urllib, datetime, struct, etc., et leurs propres bonnes pratiques, souvent détaillées dans la documentation."

Les fonctions dans le fond

Encore un principe : DRY

Pour éviter la redondance : les fonctions (et au-delà : classes et POO)

des fonctionnalités factorisées dans code mutualisé - parfois l'on peut en faire un module.

Vie plus facile : paramètres qui ont des valeurs par défaut.

Return : il vaut mieux prévoir aussi une valeur explicite retournée :

def filtrer_pair_qualite_inf(liste):
	"""modifie la liste $liste passée en argument et retourne None"""
	liste[:] = [x for x in liste if x % 2 == 0]
    
liste_originale = list(range(66))
execution = filtrer_pair_qualite_inf(liste_originale)
print(liste_originale)  # liste modifiée
print(execution)  # None
def filtrer_pair_qualite_sup(liste):
	"""retourne une copie modifiée de la liste $liste passée en argument"""
	return [x for x in liste if x % 2 == 0]

liste_originale = list(range(66))
execution = filtrer_pair_qualite_sup(liste_originale)
print(liste_originale)  # liste originale
print(execution)  # liste modifiée

Ne pas hésiter à adopter les principes de la programmation fonctionnelle (cf. partie 3).

Du côté des variables
commencer par éviter la confusion entre les noms des variables et des paramètres

 

Une autre manière (que la programmation fonctionnelle)
de préserver la valeur des variables : l'usage des constantes
(cf. fin de la partie 5 : "moyens dans la forme")

Optimisation en mémoire des collections :
éviter les éléments inutiles dans des objets itérables tels que les listes
employer des générateurs quand les éléments auront à être disponibles un par un

Optimisation algorithmique des collections :
les clés de dict liés à une interface correspondent aux identifiants des éléments du DOM

Nomenclature optimisée : les espaces de nom (pour les attributs et méthodes)

Conventions de nommage des variables dans le fond :

 

incrémentation : i et j

 

éléments des boucles : x, y, z

 

fichier dans un bloc with: f

 

variable inutilisée : _

est également alias de gettext, et a un sens dans le Shell

 

arguments hétérogènes : *args, **kwargs

for i, stuff in enumerate(foo):
for x in foo:
with open(stuff) as f:
(random.randint(10) for _ in range(10))
def foo(a, b, **kwarg):

instance en cours : self ; et classe en cours : cls

 

Exemples extraits de Le PEP8 et au delà, par la pratique

Voir la partie 5 pour les conventions de nommage sur la forme

5- Moyens pour y parvenir : dans la forme

La PEP8:

 

Style Guide for Python Code: une des plus anciennes

Le site pep8online permet de vérifier si son code respecte ce guide.

L'indentation, tabulations ou espaces

if True:
	print("True")
         print("False")

  • 4 niveaux d'indentation
  • Ne pas mélanger espace et Tab.

Longueur maximum d'une ligne

  • 80/79 caracteres max.
  • 72 pour les docstrings
  • Si vous devez découper une ligne trop longue, faites la césure après l'opérateur, pas avant.
Un_long_calcul = variable + \
		taux * 100	

Traditionnellement les terminaux des années 80 etaient limités à 80 colonnes et 24 lignes. Standard VT100:

1 ligne == 20 tabulations == 80 espaces

- parenthèse de culture générale -

Directives d'importation

import os
import math
# et non
import os, math
from random import randomint
# et surtout pas:
from random import *

sauf :

Dans l'ordre :

  • les directives d'importation faisant référence à la bibliothèque standard ;

  • les directives d'importation faisant référence à des bibliothèques tierces ;

  • les directives d'importation faisant référence à des modules de votre projet.

Le signe espace dans les expressions et instructions

pas d'espace :

  •  parenthèses, crochets et accolades :
  • virgule ou signe deux points :
  • avant la parenthèse ouvrante pour les paramètres :
  • entourer les opérateurs d'un espace  - sauf dans un paramètre :
# Oui
    spam(salade[1], {oeufs: 2})
# Non
    spam( salade[ 1 ], { oeuf: 2 } )
# Oui
    spam(1)
# Non
    spam (1)
# Oui
    if x == 4: print x, y; x, y = y, x
# Non
    if x == 4 : print x , y ; x , y = y , x
def fonction(parametre=5):
	i = i + 1
# NON
def fonction (parametre = 5):
   	i=i+1
  

La PEP 257 : de belles documentations

https://www.python.org/dev/peps/pep-0257/

Cette chaîne de caractères devient l'attribut spécial__doc__de l'objet:

fonction.__doc__
'Documentation de la fonction.'
def fonction():
    """Documentation brève sur une ligne.
    
    Documentation plus longue...
    
    """

En bref:

  • Le BDFL ( Benevolent Dictator For Life) conseille de sauter une ligne avant de fermer les docstrings, si sur plusieurs lignes.

  • On garde la syntaxe Majuscule/point a la fin.
  • Concerne les fonctions, les méthodes, les modules, les classes

 

Conventions de nommage

  • Noms à éviter: l (L minuscule), O (o majuscule) et I (i majuscule).
  • Noms des modules et packages: noms courts en minuscules. ' _ ' ok mais la PEP 8 déconseille.
  • Noms de classes: CamelCase
  • Noms d'exception: Les exceptions sont des classes, conventionnés:  'Error'
  • Fonctions et méthodes: nom_de_fonction
  • Constantes: NOM_DE_MA_CONSTANTE

Astuce: Depuis python 3.8.1, Il existe un moyen de faire des Constantes avec python:

from typing import Final
MA_CONSTANTE: Final = 1

https://docs.python.org/3/library/typing.html

6- Quand ça bloque

"Errors should never pass silently."

 

Quand le programme plante, lire le message d'erreur. Le numéro de ligne est indiqué, avec une explication.

Le cas échéant, améliorer la lisibilité du code et le sens des noms. Vérifier la qualité formelle du code (cf. outils partie 7) et des linters. Poser des print(), des points d'arrêt.

En parler à quelqu'un d'autre fait parfois émerger la solution avant la fin de la phrase.

Mais aussi utiliser les outils de débugage officiels ou de son IDE…

Pour information : pdb — The Python Debugger — Python 3.8.1 documentation

et Debugger - Features | PyCharm - JetBrains

 

"Unless explicitly silenced."

Les erreurs peuvent avoir été gérées via try. La gestion des exceptions est complète en Python. Apprendre cela au fur et à mesure que l'on en devient un programmeur confirmé ;-)

Pourquoi ?

le modèle est-il en adéquation avec la réalité ?

quels sont les types de causes envisageables ?

 

On avait fait l'hypothèse de la localisation d'une API, d'un module, d'un dossier, d'un fichier, ce que les faits viennent démentir.

L'on confond le tout et la partie (le contenu d'une clé avec le dictionnaire), ou deux parties aux noms très semblables. Ou bien l'on confond variable globale et paramètre.

L'on prend un type d'objet pour un autre, on lui applique une méthode ou un mécanisme inadéquat.

L'on oublie un 'return', ou bien d'affecter un 'return' à une variable.

L'on a fait une mauvaise estimation de la dimension d'une collection.

Le problème est-il lié à un effondrement des performances (collection sur-dimensionnée ?)

A-t-on bien conçu l'algorithme, peut-on le dessiner sans zone d'ombre ?

7- Propositions d'outils pour (bien) développer

Le linting, analyse statique du code (vs déboggueur : code à son exécution),

& vers la "conformité" au PEP 8,

& la métrique (la complexité du code ?), & le profilage (sa performance ?)…

 

 

* Welcome to Radon’s documentation!
    — Radon 2.4.0 documentation

 

Indispensable installation d'un linter sur son IDE ?

ST, Atom, PyCharm

via la gestion des packages

 

en local (réputé facile avec Anaconda) :
les outils pycodestyle, pydocstyle, pylint

Quelques sources et ressources

Un projet Python à l’épreuve de la qualité PDF

By G. Lghd

Un projet Python à l’épreuve de la qualité PDF

sujet de veille pour la formation CDA Python à Simplon : fork, version linéaire pour pdf

  • 99