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- Liste d'outils pour bien développer
  • Quelques sources

 

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

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

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

Pourquoi ce sujet ?

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

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

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

a=[0]*3

#nom de variable ésotérique
#objet délicat à  manipuler
#proposition 1
a = 4
z = 5
def az(a):  #MOCHE ET BUGUÉ :
	a+=z
az(z)
print(a)

4


#proposition 2
nombre = 4
augm = 5
def incrementer_valeur(valeur):  #MIEUX :
	"""modifies the parameter $valeur and returns it"""
	return valeur + augm
nombre = incrementer_valeur(nombre)
print(nombre)

9

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.

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

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

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.

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.

def print_items(obj):
    if not hasattr(obj, 'items'):
        return
    for item in obj.items:
        print(item)

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
  • Utiliser les fonctions max() et min() plutot que de les déterminer à l'ancienne
  • faire un for i in range(nb-1) pour ensuite utiliser des i+1 alors qu'on peut faire une boucle for i in range(1, nb) directement
  • 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
with open(pythonique, 'r') as f:
      
      for i in enumerate(f):
         longueur = abc.split(',')
          
      for i in range (1, nb):
        ...
      # et non: for i in range (nb-1):
       
      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

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

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

7- Liste d'outils pour bien développer

Quelques sources

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

By G. Lghd

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

sujet de veille pour la formation CDA Python à Simplon : fork

  • 82