| BonnesPratiques |
UserPreferences |
| Wiki Python Fr | FrontPage | RecentChanges | TitleIndex | WordIndex | SiteNavigation | HelpContents | moin.sf.net |
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.
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 2 3 4 | try:
import win32com.client
except ImportError:
raise ImportError, 'This program requires the win32all extensions for Python. See http://starship.python.net/crew/mhammond/win32/' |
Si vous avez un grand nombre de chaîne à concaténer, n'utilisez pas l'opération +.
Ne faites pas:
1 2 3 4 | a = "coucou"
resultat = ""
for i in range(100000):
resultat = resultat + a |
Ajoutez plutôt les éléments à une liste, puis fusionnez les éléments de la liste.
1 2 3 4 5 | a = "coucou"
b = []
for i in range(100000):
b.append(a)
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é.
Si vous avez des chemins des noms de fichiers à concaténer, ne faites pas:
1 2 3 | chemin = "mes fichiers" fichier = "monfichier.txt" 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 2 3 4 | import os.path chemin = "mes fichiers" fichier = "monfichier.txt" 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.
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).
Définissons une classe client. Chaque client possède simplement un numéro et un nom.
1 2 3 4 | class client:
def __init__(self,numero,nom):
self.numero = numero
self.nom = nom |
1 2 | mon_client = client(5,"Dupont") print mon_client |
<__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 2 3 4 5 6 7 | class client:
def __init__(self,numero,nom):
self.numero = numero
self.nom = nom
def __repr__(self):
return '<client id="%s" nom="%s">' % (self.numero, self.nom) |
1 2 | mon_client = client(5,"Dupont") print mon_client |
<client id="5" nom="Dupont">Ce qui est nettement plus parlant.
On peut même appliquer cela à des classes composées. Si on créé un carnet (contenant des clients):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class carnet:
def __init__(self):
self.clients = []
def ajouteClient(self, client):
self.clients.append(client)
def __repr__(self):
lignes = []
lignes.append("<carnet>")
for client in self.clients:
lignes.append(" "+repr(client))
lignes.append("</carnet>")
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 2 3 4 5 | mon_carnet = carnet() mon_carnet.ajouteClient( client(5,"Dupont") ) mon_carnet.ajouteClient( client(12,"Durand") ) print mon_carnet |
<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 s'applique sur un tas d'objets: des fichiers, des sockets...
On l'utilise souvent sans paramètre, tel que:
1 2 3 4 5 6 7 8 9 10 | # On lit un fichier:
file = open("monfichier","rb")
data = file.read()
file.close()
# On va chercher une page HTML
import urllib
url = urllib.urlopen("http://wikipython.flibuste.net")
html = url.read()
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 2 3 4 5 6 7 8 9 10 | # On lit un fichier:
file = open("monfichier","rb")
data = file.read(10000000)
file.close()
# On va chercher une page HTML
import urllib
url = urllib.urlopen("http://wikipython.flibuste.net")
html = url.read(200000)
url.close() |
Cela permet (en cas de problème) d'éviter à votre programme de partir en vrille
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 2 3 | # -*- coding: latin1 -*- good = u"Une chaîne avec des accents, correctement traitée." bad = "Une chaîne avec des accents, qui va poser des problèmes..." |
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 2 | curs=dbconn.cursor()
curs.execute("SELECT * FROM tbl WHERE id='%s'" % id) |
mais ceci :
1 2 | curs=dbconn.cursor()
curs.execute("SELECT * FROM tbl WHERE id=%s", [id]) |
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.
SELECT * FROM tbl WHERE id=''; drop table tbl; select ''
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.
SELECT * FROM tbl WHERE id='\'; drop table tbl; select \''
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.
1 2 | curs=dbconn.cursor()
curs.execute("SELECT * FROM tbl WHERE nom ILIKE %s OR prenom ILIKE %s", [nom+"%", "prenome"+%]) |
1 2 3 4 | l=raw_input("Login: ")
p=raw_input("Password: ")
curs=dbconn.cursor()
curs.execute("SELECT * FROM user WHERE login=%(lgin)s AND passwd=%(pswd)s", {"lgin": l, "pswd": p}) |
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 :
'calendrier<br>'.rstrip('<br>')
donne 'calendrie' alors qu'on aurait souhaité 'calendrier' On peut donc utiliser chacune des quatre solutions (non limitatif) suivantes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def rstringStrip1(s1,s2):
r= s1
if s1.endswith(s2):
r= s[:-len(s2)]
return r
def rstringStrip2(s1,s2):
return s1[:len(s1)-len(s2)*(s1[-len(s2):]==s2)]
def rstringStrip3(s1,s2):
return s1[:-len(s2)]+s1[-len(s2):].replace(s2,'')
def rstringStrip4(s1,s2):
import re
return re.compile(s2+'$',re.MULTILINE).sub('',s1)
#--
s= 'calendrier<br>'
print rstringStrip1(s,'<br>')
print rstringStrip2(s,'<br>')
print rstringStrip3(s,'<br>')
print rstringStrip4(s,'<br>') |
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 2 3 4 | #! python
countries = ['France','Germany','Belgium','Spain']
for i in range(0,len(countries)):
print countries[i] |
1 2 3 4 5 6 | #! python
countries = ['France','Germany','Belgium','Spain']
i = 0
while i<len(countries):
print countries[i]
i = i+1 |
Ce qui est une mauvaise idée. Vous devriez plutôt faire:
1 2 3 4 | #! python
countries = ['France','Germany','Belgium','Spain']
for country in countries:
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 2 3 4 5 | #! python
file = open('file.txt','r')
for line in file.readlines():
print line
file.close() |
1 2 3 4 5 | #! python
file = open('file.txt','r')
for line in file:
print line
file.close() |