1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
|
Bonnes pratiques en Python
--------------------------
.. sidebar:: Sommaire
.. contents:: :local:
Ce document est une petite compilation de divers éléments relatifs à Python :
* les erreurs ou maladresses que je rencontre le plus souvent quand je lis du
code Python (y compris et surtout le code écrit par moi-même) et les façons
d'y remédier.
* certains aspects du langage sur lesquels Python diffère sensiblement d'autres
langages qui permettent d'en utiliser toute la puissance.
* des conseils généraux sur les bonnes pratiques en Python.
List comprehensions
===================
Les *list comprehensions* offrent une syntaxe très concise pour construire une
liste depuis une autre liste (ou depuis un autre objet itérable). La syntaxe
est la suivante :
.. code:: python
result_list = [expression(item) for item in original_list if condition(item)]
et signifie ``result_list`` contient l'ensemble des éléments
``expression(item)`` où ``item`` est un élément de ``original_list`` pour
lequel ``condition(item)`` est vérifié. La condition qui permet de filtrer les
éléments est optionnelle.
Exemple, pour calculer les carrés des éléments d'une liste. Au lieu de :
.. code:: python
l = [1, 2, 3]
result = []
for i in l:
result.append(i*i)
qui est particulièrement inefficace à cause de l'utilisation répétée de la
fonction ``append``, on peut utiliser une *list comprehension* :
On fait :
.. code:: python
l = [1, 2, 3]
result = [i*i for i in l] # result = [1, 4, 9]
Pour calculer les racines carrées des éléments positifs (sinon ça fait boum)
d'une liste :
.. code:: python
from math import sqrt
l = [4, -3, 9]
result = [sqrt(i) for i in l if i >= 0] # result = [2, 3]
La même syntaxe existe également pour les dictionnaires, on parle alors de
*dict comprehensions* (original, n'est-ce pas ?). Par exemple pour transformer
une liste de couples nom/téléphone en un dictionnaire (pour une recherche plus
rapide) :
.. code:: python
l = [("Barthes", "0629649212"), ("Babar", "0614798553")]
d = { name : phone for (name, phone) in l }
Voir `la page
<http://docs.python.org/tutorial/datastructures.html#list-comprehensions>`__ sur
les *list comprehensions* dans la documentation officielle de python.
Les multiples facettes de ``in``
================================
Le mot-clé ``in`` a beaucoup d'usages différents et rend le code tellement
facile à écrire que les gens oublient parfois de l'utiliser.
* ``in`` offre une syntaxe universelle pour itérer sur tous les objets
itérables du langage python. On évitera les fioritures inutiles. Par exemple,
pour itérer sur une liste, au lieu de :
.. code:: python
l = [ 1, 2, 3]
for i in range(len(l)):
print l[i]
on fait simplement :
.. code:: python
l = [1, 2, 3]
for i in l:
print l[i]
de même pour itérer sur un dictionnaire, au lieu de :
.. code:: python
d = {...}
for key in d.keys():
print d[key]
on peut se contenter de :
.. code:: python
d = {...}
for key in d:
print d[key]
* ``in`` permet également de faire des tests d'appartenance dans de nombreuses
situations : appartenance d'un élément à une liste, un dictionnaire (ou
n'importe quel objet itérable), occurence d'une sous-chaîne dans une chaîne
de caractères. Par exemple :
.. code:: python
l = [line for line in open("server.log") if "Connected" in line]
va retourner la liste des lignes du fichier ``server.log`` contenant la
sous-chaîne ``Connected``.
Manipuler les listes en une seule instruction
=============================================
Plus généralement, il faut essayer au maximum de ne pas écrire de boucle
``for`` pour itérer sur les éléments d'une liste. On peut presque toujours s'en
sortir sans, c'est plus concis et surtout Python peut optimiser en interne car
il comprend qu'on essaie de faire une seule opération sur la liste entière.
Les *list comprehensions* permettent souvent de remplacer une itération par une
seule instruction. Je rappelle ici quelques fonctions utiles qui aident à se
débarrasser de boucles ``for`` inutiles :
* ``join`` pour formater une liste. Exemple, pour imprimer la liste des mots
commençants par ``a`` dans une liste de mots. Au lieu de :
.. code:: python
result = ""
l = [...]
for word in l:
if word[0] == 'a':
result += word + " "
print result
On fait :
.. code:: python
l = [...]
print " ".join([word for word in l if word[0] == 'a' ])
* ``sum``. Exemple, pour sommer les éléments positifs d'une liste :
.. code:: python
l = [2, -1, 3]
total = sum([i for in in l if i > 0])
* ``map`` pour appliquer la même fonction à tous les éléments d'une liste.
Exemple, pour inverser (au sens des palindromes) tous les mots d'une liste
:
.. code:: python
l = [ "roustine", "alicia" ]
def reverse(word):
return word[::-1]
m = map(reverse, l) # m = ['enitsuor', 'aicila']
Les *slices* sont également très utiles pour manipuler les listes en blocs.
Rappel : si ``l`` est une liste, ``l[begin:end:step]`` extrait les éléments de
``l`` depuis l'indice ``begin`` inclus à l'indice ``end`` exclu par pas de
``step``. Le ``:step`` est optionnel.
Si on laisse le paramètre ``begin`` vide, il prend la valeur 0 par défaut. De
même le paramètre ``end`` prend la valeur ``len(l)`` par défaut. Une valeur
négative pour ``begin`` ou ``end`` est décomptée depuis la fin de la liste. Par
exemple, pour extraire tous les éléments d'une liste, sauf le dernier :
.. code:: python
l = [1 , 2, 3]
m = l[:-1] # m = [1, 2]
Utiliser une valeur négative pour le paramètre ``step`` peut être utile pour
parcourir un itérable en sens inverse comme dans l'exemple donné plus haut pour
renverser un mot :
.. code:: python
word = "castor"
drow = word[::-1] # drow = "rotsac"
qui pallie au manque cruel d'une fonction ``reverse`` pour les strings.
Exceptions
==========
Les exceptions constituent un outil très puissant (souvent sous-utilisé) des
langages hauts niveaux. Elles permettent un style de programmation moins
défensif en traitant les erreurs au moment où elles surviennent plutôt qu'en
faisant des tests pour éviter de générer l'erreur.
En Python, dès que l'on essaie de faire une opération illicite (ex: accéder
à un élément en dehors d'une liste, diviser par zéro, etc.) au lieu de
simplement faire planter le programme, Python lève une exception que l'on peut
capturer, ce qui laisse une dernière chance de se rattraper avant que le
programme plante définitivement.
La syntaxe pour détecter les exceptions est la suivante :
.. code:: python
try:
... # bout de code pouvant éventuellement lever l'exception KaBoum
except Kaboum:
... # bout de code à effectuer si le bout de code précédant a levé l'exception KaBoum
Par exemple, si une ligne de code contient une division par un nombre
potentiellement (mais rarement) égal à zéro, au lieu de faire systématiquement
un test pour vérifier la non-nullité du nombre, il est beaucoup plus efficace
d'inclure la ligne dans un ``try: ... except ZeroDivisionError:`` pour gérer
spécifiquement les rares cas où le nombre est nul. C'est le principe bien connu
de *mieux vaut demander l'absolution que la permission*.
Autre exemple, quand on essaie d'accéder à un élément d'un dictionnaire qui
n'existe pas, Python lève une exception ``KeyError``. On peut utiliser cette
exception pour initialiser la valeur associée à une clé non encore existante du
dictionnaire. Par exemple, pour créer un dictionnaire des occurrences des mots
dans un texte, on voit souvent :
.. code:: python
text = "..."
result = {}
for word in text.split():
if word in result:
result[word] += 1
else:
result[word] = 1
On peut utiliser avantageusement l'exception ``KeyError`` pour éviter le test
``if`` systématique :
.. code:: python
text = "..."
result = {}
for word in text.split():
try:
result[word] += 1
except KeyError:
result[word] = 1
Voir `la page
<http://docs.python.org/tutorial/errors.html#handling-exceptions>`__ sur les
exceptions dans la documentation officielle.
Valeurs équivalentes à ``True`` ou ``False``
============================================
Si ``test`` est une variable booléenne (égale à ``True`` ou ``False``), on sait
qu'il est redondant d'écrire :
.. code:: python
if test == True:
...
alors qu'il suffit d'écrire:
.. code:: python
if test:
...
Plus généralement, Python a des règles de conversion automatique de certains
types vers les booléens pour permettre une syntaxe plus légère (et parfois un
gain en performances) pour les tests conditionnels :
* comme dans la plupart des langages, un entier strictement positif est
converti en ``True`` et zéro est converti en ``False``
* une chaîne de caractères est convertie en ``False`` si et seulement si elle
est vide. Ainsi, pour tester si une chaîne de caractères ``title`` est non
vide, on peut simplement faire :
.. code:: python
if title:
...
au lieu de :
.. code:: python
if len(title) > 0:
...
* la valeur ``None`` (qui est une constante utilisée quand on ne sait pas
quelle valeur donner à une variable) est convertie en ``False``. Pour tester
si une variable ``var`` n'est pas égale à ``None`` on peut donc simplement
faire :
.. code:: python
if not var:
...
**Attention** toutefois, ceci n'est correct que si ``var`` ne peut pas prendre de
valeurs converties en ``False`` par Python. Par exemple, si ``var`` peut être
``None`` ou une chaîne de caractères, le test ``if not var:`` ne permet pas
de distinguer le cas où ``var`` est ``None`` du cas où ``var`` est une chaîne
vide.
Générateurs
===========
Les générateurs permettent de créer facilement des itérateurs (des objets
itérables). Il en existe deux parfums différents :
* les *generator expressions* : celles-ci sont exactement
identiques aux *list comprehensions* à ceci près qu'on remplace les crochets
par des parenthèses. Ainsi le code suivant :
.. code:: python
l = [1, 2, 3]
m = (i*i for i in l)
print '\n'.join(m)
produit exactement le même résultat que si la deuxième ligne avait été
remplacée par :
.. code:: python
m = [i*i for i in l]
La différence est que dans le cas d'une *list comprehension* la liste ``m``
est construite intégralement (et placée en mémoire) au moment de la
définition de la variable ``m``. Dans le cas où ``m`` est définie par une
*generator expression*, les éléments de ``m`` sont générés de façon
paresseuse : à chaque itération de la boucle ``for``, Python revient à la
définition de ``m`` et génère un nouvel élément.
En termes de performance, les deux solutions sont équivalentes (au final
chaque élément de ``m`` est généré une et une seule fois). Les *generator
expressions* sont par contre beaucoup plus avantageuses au niveau de
l'occupation de la mémoire : puisque les éléments sont générés dynamiquement,
jamais plus d'un élément n'est stocké en mémoire au même moment. Lorsque la
liste est trop grosse pour tenir dans la mémoire, ou lorsqu'elle n'est pas
appelée à être utilisée ultérieurement dans le code, alors il est préférable
d'utiliser une *generator expression*.
Lorsque l'on utilise une *generator expression* comme argument d'une
fonction, Python autorise à ne garder qu'une seule paire de parenthèses.
Exemple :
.. code:: python
l = [1, 2, 3]
total = sum((i*i for i in l))
total2 = sum(i*i for i in l)
equal = (total == total2) # equal est égal à True
* une deuxième façon de définir un générateur est d'écrire une fonction
utilisant le mot-clé ``yield``. Prenons l'exemple de la fonction suivante :
.. code:: python
def file_minmax_generator(filename):
for line in open(filename):
l = map(int, line.split())
yield min(l), max(l)
cette fonction lit un fichier ligne à ligne. Chaque ligne contient une liste
de nombres dont on extrait le minimum et le maximum. La signification du
mot-clé ``yield`` est la suivante : *retourner la valeur indiquée et arrêter
l'exécution de la fonction (rendre la main) en attendant d'être appelé à
nouveau*. On peut alors utiliser cette fonction dans une boucle ``for`` de la
façon suivante :
.. code:: python
for (inf, sup) in file_minmax_generator(filename):
print (inf + sup)/2.
à chaque itération de la boucle ``for`` la fonction ``file_minmax_generator``
est exécutée jusqu'à rencontrer le mot-clé ``yield`` et la valeur retournée
est utilisée dans le corps de la boucle jusqu'à la prochaine itération.
* enfin, certaines fonctions prédéfinies par Python retournent des générateurs.
C'est le cas par exemple de la fonction ``xrange`` qui accepte exactement les
mêmes arguments que la fonction ``range`` (pour générer une liste de
nombres). Ainsi le code suivant :
.. code:: python
for i in xrange(10000):
...
est équivalent au même code utilisant la fonction ``range`` mais a l'avantage
de ne pas stocker en mémoire une liste de 10000 entiers (ceux-ci sont générés
dynamiquement). Il est presque toujours préférable d'utiliser la fonction
``xrange`` plutôt que la fonction ``range``, à tel point que dans la version
3.x de Python, ``range`` se comporte comme ``xrange``.
Voir `la page
<http://docs.python.org/tutorial/classes.html#generators>`__ sur les
générateurs dans la documentation officielle.
Classes avec deux méthodes dont l'une est ``__init__``
======================================================
Petit rappel sur les classes. On définit une classe de la façon suivante :
.. code:: python
class Cipher:
def __init__(self, key):
self.key = key
def decrypt(self, message):
return (message & self.key)
toutes les méthodes d'une classe prennent comme premier argument (qu'on nomme
toujours ``self`` par convention) l'instance de la classe sur laquelle la
méthode est appelée. Ainsi, si la variable ``a`` est une instance de la classe
``Cipher``, l'appel ``a.decrypt(message)`` est équivalent à ``decrypt(a,
message)``.
La fonction spéciale ``__init__`` est appelée au moment où une instance de la
classe est créée (c'est l'équivalent d'un constructeur). On l'utilise
principalement pour initialiser les attributs de l'instance. On crée une
instance de la classe ``Cipher`` ainsi :
.. code:: python
d = Cipher(key)
Un défaut assez répandu parmi les gens qui viennent des langages orientés
objets (et tout particulièrement Java) et de créer des classes pour tout et
n'importe quoi. Ceci conduit souvent à des classes qui ne contiennent que deux
méthodes, l'une d'elles étant la fonction ``__init__``. C'est le cas de la
classe donnée en exemple ci-dessus. En y regardant de plus près, on pourrait
complètement se passer d'une classe ici : il suffit d'une fonction ``decrypt``
prenant également en argument la clé (au lieu de l'obtenir comme attribut de
l'objet) :
.. code:: python
def decrypt(key, message):
return (message & key)
Certaines personnes objecteraient ceci : *oui, mais dans ce cas une classe peut
quand même faire sens, car si on veut étendre plus tard la classe Cypher
pour lui ajouter d'autres méthodes (par exemple une méthode pour encrypter)
alors on a déjà une structure en place*. Mon point de vue sur la question est
qu'il vaut mieux commencer par le code le plus simple possible, et si plus tard
il s'avère qu'on a vraiment besoin d'étendre le code, alors il n'est jamais
trop tard pour restructurer quelques fonctions pour les mettre dans une même
classe.
PEP8
====
Je ne pouvais pas terminer une liste de recommandations sans parler de la PEP8
:). Comme je l'avais déjà mentionnée, la PEP8 est un ensemble de
recommandations stylistiques sur la façon d'écrire du code Python. Sans être
une règle absolue, je trouve que suivre le style de la PEP8 donne plus de
lisibilité au code. De plus, puisque beaucoup de développeurs Python s'alignent
sur ce standard, le fait de l'appliquer réduit le décalage entre son propre
code et le code écrit par d'autres, ce qui est plus confortable au moment de
rentrer dans du code étranger.
Voici ici les points de la PEP8 qui sont, je trouve, les plus agaçants quand
ils ne sont pas suivis :
* on respecte les règles de typographie (anglaise) : pas d'espace avant les
deux points, espace après la virgule mais pas avant, etc.
* on met des espaces autour des opérateurs (comme le signe égal, le signe plus,
etc.).
* on essaie de limiter la longueur des lignes à 80 caractères maximum.
`Lire la PEP8 <http://www.python.org/dev/peps/pep-0008/>`__
|