summaryrefslogtreecommitdiffstats
path: root/content/index.rst
blob: e2d6e65061e261fa693116a06f8cbd215fdccb12 (plain)
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)````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/>`__