[PageD'Accueil] [IndexDesTitres] [IndexDesTermes

Cette page tente de lister diverses "Bonne pratiques" ("Best Pratice" en anglais) afin de faire les choses mieux, pour que vos programmes fonctionnent mieux, plus vite et soient plus portables.






Importer un module ne faisant pas partie de la distribution standard de Python

Si votre programme utilise un module ne faisant pas partie de la distribution standard de Python, il peut être difficile pour un utilisateur de deviner de quel module il s'agit et où le télécharger. En ajoutant un petit try/except au début de votre programme, vous ferez gagner du temps à vos utilisateurs, surtout si vous indiquez l'URL où aller chercher le module.

Exemple:

   1 try:
   2     import win32com.client
   3 except ImportError:
   4     raise ImportError, 'This program requires the win32all extensions for Python. See http://starship.python.net/crew/mhammond/win32/'

Concaténer des chaînes

Si vous avez un grand nombre de chaîne à concaténer, n'utilisez pas l'opération +.

Ne faites pas:

   1 a = "coucou"
   2 resultat = ""
   3 for i in range(100000):
   4     resultat  = resultat  + a

Ajoutez plutôt les éléments à une liste, puis fusionnez les éléments de la liste.

   1 a = "coucou"
   2 b = []
   3 for i in range(100000):
   4     b.append(a)
   5 resultat = "".join(b)

Cette seconde solution est des centaines de fois plus rapide que la précédente.

Est-ce encore d'actualité ? Cf. mes tests sur http://www.biologeek.com/journal/index.php/2006/01/21/85-optimisation-des-chaines-de-caracteres-en-python qui sont en contradiction avec cette astuce d'optimisation... commentaires bienvenus.

Commentaire de sebsauvage (2006-01-25): Effectivement, je constate qu'avec Python 2.4.1, cette optimisation n'est plus d'actualité.

Concaténer des chemins et des noms de fichiers

Si vous avez des chemins des noms de fichiers à concaténer, ne faites pas:

   1 chemin = "mes fichiers"
   2 fichier = "monfichier.txt"
   3 chemin_complet = chemin + "\\" + fichier 

C'est une très mauvaise façon de faire. En effet vous ne savez pas sur quelle plateforme va fonctionner votre programme.
Sous Windows, le séparateur de chemin est "\", sous Unix c'est "/" et sur Macintosh c'est ":".

Pour être sûr que votre programme fonction partout, utilisez os.path.join:

   1 import os.path
   2 chemin = "mes fichiers"
   3 fichier = "monfichier.txt"
   4 chemin_complet = os.path.join(chemin,fichier)

Pour connaître le séparateur : utilisez os.path.sep.

De même, pensez à utiliser les autres méthodes de os.path plutôt que de tenter de manipuler les chaînes vous-même.

Des objets lisibles

Quand on doit déboguer, il est parfois difficile de s'y retrouver, et d'avoir une vue claire sur l'état des objets. Voici une pratique que j'utilise presque systématiquement: créer une méthode repr dans tous mes objets.

La méthode repr permet de fournir une représentation textuelle d'un objet. C'est elle qui est appellée quand il faut convertir un objet en texte (par exemple lors d'un print).

Exemple 1

Définissons une classe client. Chaque client possède simplement un numéro et un nom.

   1 class client:
   2     def __init__(self,numero,nom):
   3         self.numero = numero
   4         self.nom = nom

Si maintenant on créé un client et qu'on l'affiche:

   1 mon_client = client(5,"Dupont")
   2 print mon_client

On obtient:

<__main__.client instance at 0x007D0E40>

Ce qui n'est pas très parlant.

Changeons notre classe client et ajoutons la méthode repr (notez les deux underscore avant et après le nom repr):

   1 class client:
   2     def __init__(self,numero,nom):
   3         self.numero = numero
   4         self.nom = nom
   5 
   6     def __repr__(self):
   7         return '<client id="%s" nom="%s">' % (self.numero, self.nom)

Maintenant, si on affiche un client:

   1 mon_client = client(5,"Dupont")
   2 print mon_client

On obtient:

<client id="5" nom="Dupont">

Ce qui est nettement plus parlant.

Exemple 2

On peut même appliquer cela à des classes composées. Si on créé un carnet (contenant des clients):

   1 class carnet:
   2     def __init__(self):
   3         self.clients = []
   4         
   5     def ajouteClient(self, client):
   6         self.clients.append(client)
   7     
   8     def __repr__(self):
   9         lignes = []
  10         lignes.append("<carnet>")
  11         for client in self.clients:
  12             lignes.append("  "+repr(client))
  13         lignes.append("</carnet>")
  14         return "\n".join(lignes)

Vous noterez qu'on a créé aussi une méthode repr qui renvoie le carnet lui-même, mais aussi chacun des clients qu'il contient.

On peut alors créer un carnet, et afficher de façon très lisible ce qu'il contient:

   1 mon_carnet = carnet()
   2 mon_carnet.ajouteClient( client(5,"Dupont") )
   3 mon_carnet.ajouteClient( client(12,"Durand") )
   4 
   5 print mon_carnet

ce qui donne:

<carnet>
  <client id="5" nom="Dupont">
  <client id="12" nom="Durand">
</carnet>

Ce genre de petite astuce - qui n'est pas exclusif à Python - pourra vous êtres d'un grand secours lorsque vous voudrez déboguer vos programmes.
Vous pouvez également utiliser cela pour afficher l'état des objets dans la clause except d'un try.

La méthode read()

La méthode read s'applique sur un tas d'objets: des fichiers, des sockets...
On l'utilise souvent sans paramètre, tel que:

   1 # On lit un fichier:
   2 file = open("monfichier","rb")
   3 data = file.read()
   4 file.close()
   5 
   6 # On va chercher une page HTML
   7 import urllib
   8 url = urllib.urlopen("http://wikipython.flibuste.net")
   9 html = url.read()
  10 url.close()

Mais imaginons que le fichier fasse 40 Giga-octets, ou que le site web vous envoie des données sans jamais s'arrêter: Votre programme va ralentir les autres programmes, planter et peut-être mettre en péril la stabilité du système parcequ'il aura consommé une énorme quantité de mémoire.

Il faut limiter la quantité de données lues. Par exemple, je ne m'attend pas à trouver des pages HTML supérieures à 200 ko. On peut donc limiter le read à 200000. De même, les fichiers que je vais manipuler ne dépasseront pas 10 Mo. Je limite donc à 10 Mo.

   1 # On lit un fichier:
   2 file = open("monfichier","rb")
   3 data = file.read(10000000)
   4 file.close()
   5 
   6 # On va chercher une page HTML
   7 import urllib
   8 url = urllib.urlopen("http://wikipython.flibuste.net")
   9 html = url.read(200000)
  10 url.close()

Cela permet (en cas de problème) d'éviter à votre programme de partir en vrille

Utiliser des chaînes avec des accents

L'utilisation d'accents dans les chaînes de caractères provoque souvent des déconvenues. Entre autre des erreurs "UnicodeEncodeError: 'ascii' codec can't encode." ou autres joyeusetées. Pour éviter cela, indiquez quel est l'encodage utilisé dans votre fichier source, et utilisez des chaînes unicode. Pour plus de détails voir JouerAvecUnicode.

   1 # -*- coding: latin1 -*-
   2 good = u"Une chaîne avec des accents, correctement traitée."
   3 bad = "Une chaîne avec des accents, qui va poser des problèmes..."

Exécuter une requête SQL

Faille potentielle

La méthode la plus "naturelle" de lancer une requête introduira une faille de sécurité dans votre programme si les paramètres de la requête viennent de l'extérieur (script CGI par exemple).

Ainsi, vous ne devez par faire :

   1   curs=dbconn.cursor()
   2   curs.execute("SELECT * FROM tbl WHERE id='%s'" % id)

mais ceci :

   1   curs=dbconn.cursor()
   2   curs.execute("SELECT * FROM tbl WHERE id=%s", [id])

Explications

Que se passe-t-il si un utilisateur mal intentionné donne comme à la variable id la valeur "'; drop table tbl; select '" dans la requête "SELECT * FROM tbl WHERE id=%s" ?

Si vous avez utilisé la mauvaise méthode, la requête exécutée provient d'une simple concaténation de chaine qui produit le résultat suivant. La requête obtenue aura des conséquences catastrophiques, puisqu'une table sera détruite.

Si vous avez utilisez la bonne méthode, les caractères spéciaux de la variable id sont échappés par l'appel de execute. La requête obtenue n'est plus dangereuse et ne retournera probablement aucun enregistrement.

Les caractères spéciaux sont préfixés par des caractères d'échappements par la méthode execute, évitant tout risque de pollution de votre requête par des données de l'extérieur.

Cette attaque est connue sous le nom de "SQL injection"

De plus, il est possible que le module de base de données tente d'optimiser la vitesse d'exécution en mettant en cache les "compilations" des requêtes précédentes. En introduisant des variables dans la requêtes, on empêche ce mécanisme d'agir, car toutes les requêtes sont différentes au yeux du cache. Avec la bonne méthode, les requêtes sont constantes, et on gagne en temps d'exécution si on ré-exécute la même requete avec des paramètres différents.

Autres cas

Eliminer un suffixe dans une chaîne

Il y a plusieurs solutions pour éliminer un suffixe dans une chaîne de caractères. On ne peut pas utiliser rstrip qui est prévu pour éliminer un ensemble de caractères suffixes. Exemple :

donne 'calendrie' alors qu'on aurait souhaité 'calendrier' On peut donc utiliser chacune des quatre solutions (non limitatif) suivantes:

   1 def rstringStrip1(s1,s2):
   2     r= s1
   3     if s1.endswith(s2):
   4         r= s[:-len(s2)]
   5     return r
   6 def rstringStrip2(s1,s2):
   7     return s1[:len(s1)-len(s2)*(s1[-len(s2):]==s2)]
   8 def rstringStrip3(s1,s2):
   9     return s1[:-len(s2)]+s1[-len(s2):].replace(s2,'')
  10 def rstringStrip4(s1,s2):
  11     import re
  12     return re.compile(s2+'$',re.MULTILINE).sub('',s1)
  13 #--
  14 s= 'calendrier<br>'
  15 print rstringStrip1(s,'<br>')
  16 print rstringStrip2(s,'<br>')
  17 print rstringStrip3(s,'<br>')
  18 print rstringStrip4(s,'<br>')

Boucle for et itérateurs

Quand on vient d'autres langages, on est tenté d'utiliser les structures de ces autres langages. Par exemple, pour itérer sur une liste, on serait tenté de faire:

   1 countries = ['France','Germany','Belgium','Spain']
   2 for i in range(0,len(countries)):
   3     print countries[i]

ou bien

   1 countries = ['France','Germany','Belgium','Spain']
   2 i = 0
   3 while i<len(countries):
   4     print countries[i]
   5     i = i+1

Ce qui est une mauvaise idée. Vous devriez plutôt faire:

   1 countries = ['France','Germany','Belgium','Spain']
   2 for country in countries:
   3     print country

Le résultat est le même, mais:

De même, pour lire les lignes d'un fichier texte, ne faites pas:

   1 file = open('file.txt','r')
   2 for line in file.readlines():
   3     print line
   4 file.close()

mais plutôt:

   1 file = open('file.txt','r')
   2 for line in file:
   3     print line
   4 file.close()

C'est plus "Pythonique".

Quelques règles de bonne conduite

Le langage Python a été conçu pour que le rédacteur (souvent programmeur ou développeur, mais pas nécessairement) puisse exprimer clairement ce qu'il pense.

Comme dans tout autre langage, on peut aussi s'exprimer très obscurément. Si tel est votre but ...

Les bonnes habitudes sont énoncées dans la PEP 8 (Python Enhancement Proposal, propositions d'améliorations en Python). Il faut absolument les respecter. Les avantages ne deviennent évidents qu'au bout de quelques semaines.

Comme autres PEP, je mentionne les numéros 20, 257 et 263.

J'utilise pylint. C'est le contrôleur avec lequel je me suis bien entendu; en outre il me donne de très bonnes notes.

pychecker est un autre contrôleur qui va jusqu'à descendre dans les radicules de numpy voir si tous les modules sont bien là, même si on n'en a pas besoin.

Sans commentaire.

Par exemple le site de Laurent Pointal: http://www.limsi.fr/Individu/pointal/python-works.html avec une liste de liens http://www.limsi.fr/Individu/pointal/python.html

Sans commentaire.

Ce qu'on en dit ailleurs

http://sis36.berkeley.edu/projects/streek/agile/bad-smells-in-code.html

Mailing list

Une mailing-list francophone est gérée sur le site de http://www.aful.org


2016-06-05 21:42