diff options
Diffstat (limited to 'content')
| -rw-r--r-- | content/index.rst | 498 |
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/>`__ |
