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 `__ 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 `__ 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 `__ 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 `__