summaryrefslogtreecommitdiffstats
path: root/content/index.rst
diff options
context:
space:
mode:
Diffstat (limited to 'content/index.rst')
-rw-r--r--content/index.rst498
1 files changed, 498 insertions, 0 deletions
diff --git a/content/index.rst b/content/index.rst
new file mode 100644
index 0000000..e2d6e65
--- /dev/null
+++ b/content/index.rst
@@ -0,0 +1,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/>`__