Repository with sources and generator of https://larlet.fr/david/ https://larlet.fr/david/
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

article.md 53KB

title: Bonnes pratiques et astuces Python slug: bonnes-pratiques-et-astuces-python date: 2008-05-11 20:50:44 type: post vignette: images/logos/python_nouveau.png contextual_title1: ★ Astuces et bonnes pratiques Django contextual_url1: 20080211-astuces-et-bonnes-pratiques-django contextual_title2: Présentation de Django aux journées Python francophones contextual_url2: 20070519-presentation-de-django-aux-journees-python-francophones contextual_title3: ★ Pourquoi programmer en Python ? contextual_url3: 20060505-pourquoi-programmer-en-python

Ça faisait un moment que je n’avais pas parlé des bonnes pratiques Python mais l’approche de Pycon fr (où je présenterai Django : le pourquoi et le comment le 18 mai), l’événement Python incontournable avec un programme des plus alléchants, m’a bien motivé pour effectuer la traduction de l’une des meilleures présentation par David Goodger que je connaisse qui remet les bonnes pratiques Python à plat, ce qui est toujours bon avant d’aller plus loin.

La lisibilité est importante

Les programmes doivent être écrits pour être lus par des gens et accidentellement exécutés par les machines.

-- Abelson & Sussman, Structure and Interpretation of Computer Programs

Essayez de rendre vos programmes faciles à lire et évidents.

PEP 8 : Style Guide pour le code Python

Une lecture immanquable : http://www.python.org/dev/peps/pep-0008/ (PEP = Python Enhancement Proposal)

Un PEP est une document procurant des informations à la communauté Python, ou décrivant une nouvelle fonctionnalité de Python et ses processus ou de son environnement.

La communauté Python a ses propres standards sur ce à quoi doit ressembler le code, codifiés dans le PEP8. Ces standards sont différents de ceux des autres communautés, comme C, Java, etc.

L’indentation et les espaces étant si importants en Python, ce Style Guide est une standard. Il est important que vous adhériez au guide ! La plupart des projets suivent ces conventions.

Whitespace 1

  • 4 espaces par niveau d’indentation.
  • Pas de tabs.
  • Ne jamais mixer des tabs et des espaces.
  • Un saut de ligne entre les fonctions.
  • Deux sauts de ligne entre les classes.

Whitespace 2

  • Ajoutez un espace après “, “, dans les dictionnaires, les listes, les tuples, les arguments d’une liste d’arguments et après “:” dans les dictionnaires mais pas avant.
  • Mettez des espaces autour des assignements et des comparaisons (excepté pour les arguments d’une liste).
  • Pas d’espace aux ouvertures/fermetures de parenthèses ou juste avant une liste d’arguments.
  • Pas d’espace en ouverture/fermeture de docstrings.

    def make_squares(key, value=0):
        """Return a dictionary and a list..."""
        d = {key: value}
        l = [key, value]
        return d, l
    

Nommage

  • joined_lower pour les fonctions, méthodes et attributs
  • joined_lower ou ALL_CAPS pour les constantes
  • StudlyCaps pour les classes
  • camelCase seulement pour suivre des conventions pré-existantes
  • Attributs: interface, _internal, __private

Mais essayez d’éviter la forme __privée. Je ne l’utilise jamais. Faites moi confiance. Si vous l’utilisez, vous le regretterez plus tard.

Longues lignes et continuité

Garder une taille de ligne inférieure à 80 caractères.

Utilisez la continuité implicite des lignes au sein des parenthèses/crochets/accolades :

def __init__(self, first, second, third,
             fourth, fifth, sixth):
    output = (first + second + third
              + fourth + fifth + sixth)

Utilisez les backslashs en dernier recours :

VeryLong.left_hand_side \
    = even_longer.right_hand_side()

Les backslashs sont locaux, ils doivent terminer la ligne sur laquelle ils sont. Si vous ajoutez un espace après le backslash, ça ne sert à rien. Ah aussi, c’est laid.

Longues chaînes de caractères

Les chaînes de caractères adjacentes sont concaténées par le parser:

>>> print 'o' 'n' "e"
one

Les espaces entre les chaînes ne sont pas requis, mais aident à la lisibilité. Tous les types de quotes sont utilisable :

>>> print 't' r'\/\/' """o"""
t\/\/o

La chaîne précédée par “r” est une chaîne de type “raw”. Les backslashs ne sont pas évalués comme étant des caractères d’échappement dans les chaînes de type raw. Elles sont utiles pour les expressions régulières et les chemins de fichiers Windows.

Notez que les chaînes de caractères nommées ne sont pas concaténées :

>>> a = 'three'
>>> b = 'four'
>>> a b
  File "<stdin>", line 1
    a b
      ^
SyntaxError: invalid syntax

Cela vient du fait que la concaténation automatique est une fonctionnalité du parser/compiler Python, pas de l’interpréteur. Vous devez utiliser le signe “+” pour concaténer des chaînes de caractères à l’éxecution.

text = ('Long strings can be made up '
        'of several shorter strings.')

Les parenthèses autorisent la continuité implicite des lignes. Les chaînes de caractères sur plusieurs lignes utilisent les triple quotes :

"""Triple
double
quotes"""

'''\
Triple
single
quotes\
'''

Dans le dernier exemple ci-dessus (simple triple quotes), notez l’utilisation du backslash pour échapper les nouvelles lignes. Cela élimine les nouvelles lignes en conservant les quotes joliment alignées à gauche. Les backslashs doivent être à la fin de leurs lignes.

Déclarations

Bon :

if foo == 'blah':
    do_something()
do_one()
do_two()
do_three()

Mauvais :

if foo == 'blah': do_something()
do_one(); do_two(); do_three()

Les espaces et l’indentation sont de bons indicateurs visuels du flot du programme. L’indentation de la seconde ligne du “Bon” ci-dessus montre au lecteur que quelque chose va se produire, alors que le manque d’indentation dans le “Mauvais” exemple cache le “if”.

Les déclarations multiples sur une même ligne sont une torture. En Python, la lisibilité compte.

Docstrings et Commentaires

Docstrings = Comment utiliser le code

Commentaires = Pourquoi (rationnel) et comment le code fonctionne

Les docstrings expliquent comment utiliser le code et sont là pour les utilisateurs de votre code. Quelques usages :

  • Expliquer le but d’une fonction même si ça vous semble évident car ça ne semblera pas forcément évident à une personne plus tard.
  • Décrire les paramètres attendus, les valeurs retournées et les exceptions levées.
  • Si la méthode est fortement couplée à un seul appelant, mentionner la fonction appelante (attention au fait que celle-ci puisse changer).

Les commentaires expliquent pourquoi et sont pour les mainteneurs de votre code. Examples incluant des notes pour vous-même, comme :

# !!! BUG: ...

# !!! FIX: This is a hack

# ??? Why is this here?

Les deux types sont de votre ressort donc écrivez de bonnes docstrings et de bons commentaires !

Les docstrings sont utiles pour un usage interactif (help()) et pour les systèmes d’auto-documentation.

Les commentaires et docstrings faux sont pire que tout. Donc conservez les à jour ! Lorsque vous effectuez des modifications, assurez vous que les commentaires et les docstrings sont cohérents avec le code.

Il y a un PEP entier consacré aux docstrings, PEP 257, “Docstring Conventions”.

La pratique a raison de la théorie

Il y a toujours des exceptions. Issu du PEP 8 :

Mais plus important : sachez être pertinents - parfois le style guide ne s’applique pas. Lorsque vous avez un doute, utilisez votre raison. Étudiez d’autres possibilités et décidez de ce qui vous semble le mieux. Et n’hésitez pas à demander ! Deux bonnes raisons de ne pas suivre une règle particulière :

(1) Lorsque appliquer la règle va rendre le code moins lisible, même pour quelqu’un qui est habitué à lire du code qui suit les règles.

(2) Pour être cohérent avec du code préexistant qui enfreint aussi ces règles (peut-être pour des raisons historiques) -- même si c’est aussi une opportunité pour faire un peu de nettoyage (dans un pur style XP).

… mais la pratique ne doit pas réduire la théorie à néant !

On plonge maintenant au cœur du tutoriel : les astuces. On va commencer avec les plus faciles et augmenter progressivement le niveau.

Variables intermédiaires

Dans les autres langages :

temp = a
a = b
b = temp

En Python :

b, a = a, b

Vous l’avez peut-être déjà rencontré mais savez vous comment ça fonctionne ?

  • La virgule est la syntaxe de construction du tuple.
  • Un tuple est créé à droite (tuple packing).
  • Un tuple en est la cible à gauche (tuple unpacking).

La partie à droite est unpackée dans les noms de tuple de la partie à gauche.

D’autres exemples:

>>> l =['David', 'Pythonista', '+1-514-555-1234']
>>> name, title, phone = l
>>> name
'David'
>>> title
'Pythonista'
>>> phone
'+1-514-555-1234'

Utile dans les boucles sur des données structurées (la variable l ci-dessus a été conservée) :

>>> people = [l, ['Guido', 'BDFL', 'unlisted']]
>>> for (name, title, phone) in people:
...     print name, phone
...
David +1-514-555-1234
Guido unlisted

Chaque item de people est unpacké dans le tuple (name, title, phone).

Il est aussi possible de faire le chemin inverse, il faut juste s’assurer d’avoir la même structure à droite et à gauche :

>>> david, (gname, gtitle, gphone) = people
>>> gname
'Guido'
>>> gtitle
'BDFL'
>>> gphone
'unlisted'
>>> david
['David', 'Pythonista', '+1-514-555-1234']

Aller plus loin avec les tuples

On a vu que la virgule était le constructeur du tuple, pas les parenthèses. Par exemple :

>>> 1,
(1,)

L’interpréteur Python montre les parenthèses pour que ce soit plus clair et je vous conseille de faire de même :

>>> (1,)
(1,)

Mais n’oubliez pas la virgule !

>>> (1)
1

Dans un tuple contenant un seul élément, la virgule est nécessaire. Dans un tuple avec plus de 2 éléments, la virgule finale est optionnelle. Pour un tuple vide, une paire de parenthèses suffit :

>>> ()
()

>>> tuple()
()

Une erreur de typo courante est de laisser une virgule alors que vous ne souhaitez pas avoir un tuple. Il est très facile de l’oublier dans votre code :

>>> value = 1,
>>> value
(1,)

Donc si vous vous retrouvez avec un tuple alors que vous ne vous y attendiez pas, cherchez la virgule ! (Note du traducteur : de ma propre expérience, il est plus courant d’oublier la virgule pour un tuple ne contenant qu’un seul élément, dans les settings de Django par exemple, cherchez plutôt la virgule manquante dans ces cas là).

Le “_” interactif

C’est une fonctionnalité très utile que peu de développeurs connaissent. (Note du traducteur : bien entendu vous n’en faites pas partie et vous connaissez les dangers associés.)

Dans un interpréteur interactif, que vous évaluiez une expression ou que vous appeliez une fonction, le résultat est stocké dans une variable temporaire, _ (un underscore) :

>>> 1 + 1
2
>>> _
2

_ stocke la dernière valeur affichée.

Lorsqu’un résultat vaut None, rien n’est affiché, donc _ ne change pas. C’est normal !

Ça ne marche que dans un interpréteur interactif, pas dans un module.

C’est particulièrement utile lorsque vous travaillez sur un problème de manière interactive, et que vous souhaitez stocker la valeur du dernier résultat :

>>> import math
>>> math.pi / 3
1.0471975511965976
>>> angle = _
>>> math.cos(angle)
0.50000000000000011
>>> _
0.50000000000000011

Construction de chaînes de caractères

Commençons avec une liste de chaînes de caractères :

colors = ['red', 'blue', 'green', 'yellow']

On veut concaténer ces chaînes ensemble pour en créer une longue. Particulièrement lorsque le nombre de sous-chaînes est gros…

Ne faites pas :

result = ''
for s in colors:
    result += s

C’est très lent. Ça utilise énormément de mémoire et de performances. La somme va additionner, stocker, et ensuite passer à la suite pour chaque étape intermédiaire.

Faites plutôt ceci :

result = ''.join(colors)

La méthode join() fait toute la copie en une seule passe.

Lorsque vous ne traitez qu’une petite centaine de chaînes de caractères, ça ne fait aucune différence. Mais prenez l’habitude de construire vos chaînes de façon optimale, car avec des milliers ou des boucles, ça va faire la différence.

Construire des chaînes, solutions 1

Voici quelques techniques pour utiliser la méthode join().

Si vous voulez un espace comme séparateur :

result = ' '.join(colors)

ou une virgule et un espace :

result = ', '.join(colors)

voici un cas courant d’utilisation :

colors = ['red', 'blue', 'green', 'yellow']
print 'Choose', ', '.join(colors[:-1]), \
      'or', colors[-1]

Pour faire une phrase grammaticalement correcte, on veut des virgules entre chaque valeurs sauf la dernière, où l’on préfère un “ou”. La syntaxe de découpage d’une liste s’occupe du reste. La “partie jusqu’à -1” ([:-1]) retourne tout sauf la dernière valeur, que l’on peut concaténer avec nos virgules.

Bien sûr, ce code ne fonctionnera pas avec les cas particuliers comme une liste de taille 0 ou 1. Ce qui retourne :

Choose red, blue, green or yellow

Construire des chaînes, solutions 2

Vous avez besoin d’appliquer une fonction pour générer les chaînes initiales :

result = ''.join(fn(i) for i in items)

Ça utilise une generator expression, dont on parlera plus tard.

Si vous devez modifier les chaînes de manière incrémentale, commencez par les stocker dans une liste pour commencer :

items = []
...
items.append(item)  # de nombreuses fois
...
# une fois la liste complétée
result = ''.join(fn(i) for i in items)

On accumule les parties de la liste afin de pouvoir appliquer le join, ce qui est plus rapide.

Utilisez in lorsque c’est possible (1)

Bon :

for key in d:
    print key
  • in est généralement plus rapide.
  • Ce pattern marche aussi pour des items dans des containers arbitraires (comme les listes, les tuples ou les tests).
  • in est aussi un opérateur (comme on va le voir).

Mauvais :

for key in d.keys():
    print key

C’est limité aux objects ayant une méthode keys().

Utilisez in lorsque c’est possible (2)

Mais .keys() est nécessaire lorsque vous modifiez le dictionnaire :

for key in d.keys():
    d[str(key)] = d[key]

d.keys() crée une liste statique des clés du dictionnaire. Sinon, vous allez lever une exception “RuntimeError: dictionary changed size during iteration”.

Utilisez key in dict, et non dict.has_key() :

# faites ça :
if key in d:
    ...do something with d[key]

# mais pas ça :
if d.has_key(key):
    ...do something with d[key]

in est ici utilisé comme un opérateur.

La méthode get des dictionnaires

On doit souvent initialiser les entrées d’un dictionnaire avant de les utiliser:

Voici la manière naïve de faire :

navs = {}
for (portfolio, equity, position) in data:
    if portfolio not in navs:
        navs[portfolio] = 0
    navs[portfolio] += position * prices[equity]

dict.get(key, default) permet de ne pas avoir à se soucier du test :

navs = {}
for (portfolio, equity, position) in data:
    navs[portfolio] = (navs.get(portfolio, 0)
                       + position * prices[equity])

Beaucoup mieux.

La méthode setdefault des dictionnaires (1)

Ici on doit initialiser les valeurs d’un dictionnaire mutables. Chaque valeur du dictionnaire sera une liste. Voici la manière naïve :

equities = {}
for (portfolio, equity) in data:
    if portfolio in equities:
        equities[portfolio].append(equity)
    else:
        equities[portfolio] = [equity]

dict.setdefault(key, default) s’occupe de ça de manière beaucoup plus rapide :

equities = {}
for (portfolio, equity) in data:
    equities.setdefault(portfolio, []).append(equity)

dict.setdefault() est équivalent à “get ou set & get”. Ou “set si nécessaire, puis get”. C’est particulièrement rapide si votre clé de dictionnaire est coûteuse à générer ou longue à taper.

Le seul problème avec dict.setdefault() c’est que la valeur par défaut est évaluée, qu’elle soit utilisée ou non. Ça ne pose problème que si la clé est coûteuse à calculer.

Si la valeur par défaut est coûteuse à calculer, vous devriez plutôt utiliser la classe defaultdict.

La méthode setdefault des dictionnaires (2)

On va voir qu’il est possible d’utiliser setdefault pour déclarer une valeur par défaut :

navs = {}
for (portfolio, equity, position) in data:
    navs.setdefault(portfolio, 0)
    navs[portfolio] += position * prices[equity]

La méthode setdefault d’un dictionnaire retourne la valeur par défaut, mais nous l’ignorons ici. On tire profit d’une conséquence de l’utilisation de setdefault, la valeur n’est initialisée que si elle n’existe pas déjà.

defaultdict

Nouveau avec Python 2.5.

defaultdict est nouveau dans Python 2.5, il fait partie du module collections. defaultdict est identique aux dictionnaires classiques, excepté pour deux cas :

  • il prend un premier argument optionnel : une fonction factory par défaut
  • lorsqu’une clé de dictionnaire est rencontrée pour la première fois, la fonction factory par défaut est appelée et le résultat initialise la valeur du dictionnaire.

Il y a deux manières d’accéder à defaultdict :

  • importer le module collections et l’appeler à travers le module :

    import collections
    d = collections.defaultdict(...)
    
  • ou importer defaultdict directement :

    from collections import defaultdict
    d = defaultdict(...)
    

Voici l’exemple déjà traité, où chaque valeur du dictionnaire fois être initialisé pour être une liste vide, réécrit en utilisant defaultdict :

from collections import defaultdict

equities = defaultdict(list)
for (portfolio, equity) in data:
    equities[portfolio].append(equity)

Il n’y a plus d’astuce ici. Dans ce cas, la fonction factory par défaut est list, ce qui retourne une liste vide.

C’est la manière d’avoir un dictionnaire avec les valeurs par défaut à 0, utilisez int comme factory :

navs = defaultdict(int)
for (portfolio, equity, position) in data:
    navs[portfolio] += position * prices[equity]

Il faut faire attention à defaultdict quand même. Vous ne pouvez pas utiliser l’exception KeyError sur un dictionnaire initialisé avec defaultdict. Vous devez utiliser la condition “key in dict” si vous voulez vérifier l’existence d’une clé de manière spécifique.

Construire et scinder des dictionnaires

Voila une technique utile pour construire un dictionnaire à partir de deux listes (ou séquences), une liste pour les clés, une liste pour les valeurs :

given = ['John', 'Eric', 'Terry', 'Michael']
family = ['Cleese', 'Idle', 'Gilliam', 'Palin']
pythons = dict(zip(given, family))
>>> pprint.pprint(pythons)
{'John': 'Cleese',
 'Michael': 'Palin',
 'Eric': 'Idle',
 'Terry': 'Gilliam'}

L’inverse est trivial bien sûr :

 >>> pythons.keys()
 ['John', 'Michael', 'Eric', 'Terry']
 >>> pythons.values()
 ['Cleese', 'Palin', 'Idle', 'Gilliam']

Notez que l’ordre du résultat des .keys() et .values() à est différent des listes utilisées lors de la création du dictionnaire. L’ordre d’entrée est différent de l’ordre de sortie car un dictionnaire n’est pas ordonné. Par contre, l’ordre des clés est consistant avec celui des valeurs, à condition que le dictionnaire n’ait pas été modifié entre temps.

Tester des valeurs vraies

Il est élégant et rapide de tirer partie des avantages de Python en ce qui concerne les valeurs booléennes :

# faites ça :     # et pas ça :
if x:             if x == True:
    pass              pass

Test d’une liste :

# faites ça :     # et pas ça :
if items:         if len(items) != 0:
    pass              pass

                  # et surtout pas ça :
                  if items != []:
                      pass

Valeurs vraies

Les noms True et False sont des instances intrinsèques à Python de type bool, des valeurs booléennes. Comme None, il n’existe qu’une seule instance de chaque.

False True
False (== 0) True (== 1)
"" (empty string) toutes les chaînes à part "" (" ", "nimportequoi")
0, 0.0 n'importe quel chiffre à part 0 (1, 0.1, -1, 3.14)
[], (), {}, set() n'importe quel container non vide ([0], (None,), [''])
None pratiquement tous les objets qui ne sont explicitement équivalents à False

Voici par exemple un objet qui est toujours vrai :

>>> class C:
...  pass
...
>>> o = C()
>>> bool(o)
True
>>> bool(C)
True

Pour contrôler la valeur booléenne d’une instance ou d’une classe définie, utilisez les méthodes spéciales __nonzero__ ou __len__. Utilisez __len__ si votre classe est un container qui a une taille :

class MyContainer(object):

    def __init__(self, data):
        self.data = data

    def __len__(self):
        """Return my length."""
        return len(self.data)

Si votre classe n’est pas un container, utilisez __nonzero__ :

class MyClass(object):

    def __init__(self, value):
        self.value = value

    def __nonzero__(self):
        """Return my truth value (True or False)."""
        # This could be arbitrarily complex:
        return bool(self.value)

En Python 3.0, __nonzero__ a été renommé __bool__ afin d’être consistant avec le type bool natif. Pour être compatible, ajoutez ceci à la définition de votre classe :

__bool__ = __nonzero__

Index & Item (1)

Voici une manière élégante de vous épargner quelques lignes si vous avez besoin d’une liste de mots :

>>> items = 'zero one two three'.split()
>>> print items
['zero', 'one', 'two', 'three']

Prenons l’exemple d’un itération entre les items d’une liste, pour laquelle nous voulons à la fois l’item et la position (l’index) de cet item dans la liste :

                  - ou -
i = 0
for item in items:      for i in range(len(items)):
    print i, item               print i, items[i]
    i += 1

Index & Item (2): enumerate

La fonction enumerate prend une liste et retourne des paires (index, item) :

>>> print list(enumerate(items))
[(0, 'zero'), (1, 'one'), (2, 'two'), (3, 'three')]

Il est nécessaire d’avoir recours à une list pour afficher les résultats car enumerate est une fonction fainéante, générant un item (une paire) à la fois, seulement lorsqu’il est demandé. Une boucle for nécessite un tel mécanisme. enumerate est un exemple de générateur dont on parlera plus tard des détails. print ne prend pas un résultat à la fois mais doit être en possession de la totalité du message à afficher. On a donc converti automatiquement le générateur en une liste avant d’utiliser print.

Notre boucle devient beaucoup plus simple :

for (index, item) in enumerate(items):
    print index, item

# comparé à :              # comparé à :
index = 0               for i in range(len(items)):
for item in items:              print i, items[i]
    print index, item
    index += 1

La version avec enumerate est plus courte et plus simple que la version de gauche, et plus facile à lire que les deux autres.

Un exemple montrant que la fonction enumerate retourne un itérateur (un générateur est une sorte d’itérateur) :

>>> enumerate(items)
<enumerate object at 0x011EA1C0>
>>> e = enumerate(items)
>>> e.next()
(0, 'zero')
>>> e.next()
(1, 'one')
>>> e.next()
(2, 'two')
>>> e.next()
(3, 'three')
>>> e.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration

Les autres langages ont des “variables”

Dans de nombreux autres langages, assigner une variable revient à mettre une valeur dans une boîte.

int a = 1;

Boîte 1 a

La boîte “a” contient maintenant un integer 1.

Assigner une autre valeur à la même variable remplace le contenu de la boîte :

a = 2;

Boîte 2 a

Maintenant la boîte “a” contient un integer 2.

Assigner une variable à une autre crée une copie de la valeur et la met dans une nouvelle boîte :

int b = a;

Boîte 2 a

Boîte 2 b

“b” est une seconde boîte, avec une copie de l’entier 2. La boîte “a” en a une copie séparée.

Python a des “noms”

En Python, un “nom” ou “identifiant” est comme une étiquette attachée à un objet.

a = 1

Tag 1 a

Ici, un objet integer 1 a une étiquette appelée “a”.

Si on réassigne “a”, on déplace juste l’étiquette sur un autre objet :

a = 2

Tag 2 a

1

Maintenant le nom “a” est attaché à un objet entier 2.

L’objet entier 1 n’a plus le tag “a”. Il peut encore exister mais on n’y a plus accès via le nom “a”. (Lorsqu’un objet n’a plus aucune référence ou étiquette, il est supprimé de la mémoire.)

Si l’on assigne un nom à un autre, on attache juste une autre étiquette à un objet existant :

b = a

Tag 2 a b

Le nom “b” est juste une seconde étiquette attachée au même objet que “a”.

Bien que l’on réfère communément aux “variables” en Python (car c’est une terminologie commune aux autres langages), on manipule vraiment de “noms” ou “identifiants”. En Python, les “variables” sont des étiquettes pour des valeurs, non des boîtes nommés.

Si vous ne comprenez rien au reste de ce tutoriel, j’espère que vous aurez$ au moins retenu la façon dont les noms fonctionnent. Une bonne compréhension vous permettra d’apprendre rapidement et d’éviter des erreurs comme celle-ci:

Valeurs de paramètres par défaut

C’est une erreur courante que les débutants font souvent. Même les développeurs plus expérimentés la font s’ils n’ont pas compris comment fonctionnent les noms en Python.

def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

Le problème ici c’est que la valeur par défaut a_list, une liste vide, est évaluée lors de la définition de la fonction. Ainsi à chaque fois que vous appelez la fonction vous obtenez la même valeur par défaut. Essayez plusieurs fois :

>>> print bad_append('one')
['one']

>>> print bad_append('two')
['one', 'two']

Les listes sont modifiables, vous pouvez modifier leur contenu. La bonne manière d’avoir une liste par défaut (ou dictionnaire, ou set) est de la créer au moment du lancement, au sein de la fonction :

def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list

Formattage des chaînes de caractères avec %

L’opérateur % fonctionne en Python comme la fonction sprintf de C.

Bien sûr si vous ne connaissez pas C, ça ne vous aide pas. Pour faire simple, vous définissez un template ou format et des valeurs qui seront interprétées.

Dans cet exemple, le template contient deux spécifications de conversion “%s” signifie “insérer une chaîne de caractère ici” et “%i” signifie “convertir un integer en string et l’insérer ici”. “%s” est particulièrement utile car il utilise la fonction standard str() pour convertir un objet en une chaîne de caractères.

Les valeurs proposées doivent correspondrent au template, on a deux valeurs ici, un tuple.

name = 'David'
messages = 3
text = ('Hello %s, you have %i messages'
        % (name, messages))
print text

Ce qui donne :

Hello David, you have 3 messages

Les détails sont dans la Python Library Reference, section 2.3.6.2, “String Formatting Operations”. Mettez cette page en favoris !

Si vous ne l’avez pas encore fait, allez sur python.org, téléchargez la documentation en HTML (dans un .zip ou une archive), et installez la sur votre machine. Il n’y a rien de mieux que d’avoir la ressource de référence à portée de clavier.

Formattage des chaînes de caractères avancé

Pourquoi est-ce qu’il y a autant de personnes qui ne réalisent pas qu’il y a d’autres façons de formater les chaînes de caractères qui peuvent s’avérer plus puissantes ?

Avec des noms grâce à un dictionnaire :

values = {'name': name, 'messages': messages}
print ('Hello %(name)s, you have %(messages)i '
       'messages' % values)

Ici on a spécifié les noms des valeurs interprétées, qui constituent les clés du dictionnaire.

Vous trouvez qu’il y a de la redondance ? Les noms “name” et “messages” sont déjà définis localement. On peut en tirer parti.

En utilisant les variables locales :

print ('Hello %(name)s, you have %(messages)i '
       'messages' % locals())

La fonction locals() retourne un dictionnaire de toutes les variables locales disponibles.

C’est très puissant. Grâce à ça, vous pouvez formater toutes les chaînes de caractères que vous voulez sans avoir à vous soucier de la correspondance positionnelle avec les valeurs soumises en argument.

Mais le pouvoir peut être dangereux. (“With great power comes great responsibility.“) Si vous utilisez locals() avec un template issu d’une ressource externe, vous exposez l’intégralité de votre espace de noms local. C’est une chose à garder en tête.

Pour examiner votre espace de nom local :

>>> from pprint import pprint
>>> pprint(locals())

pprint est un module très utile. Si vous ne le connaissiez pas déjà, essayez de jouer avec. Ça rend le debugging des données structurées beaucoup plus simple !

Formattage des chaînes de caractères avancé

L’espace de nom des attributs d’une instance d’objet est simplement un dictionnaire, self.__dict__.

En utilisant l’espace de nom d’une instance :

print ("We found %(error_count)d errors"
       % self.__dict__)

Equivalent à, mais moins flexible que :

print ("We found %d errors"
       % self.error_count)

Note: Les attributs d’une classe sont dans le __dict__ de la classe. Les espaces de noms sont hérités et constituent donc des dictionnaires chaînés.

List Comprehensions

Les list comprehensions (“listcomps” pour les intimes) sont des raccourcis syntaxiques pour ce pattern généralement utilisé.

La manière traditionnelle avec for et if :

new_list = []
for item in a_list:
    if condition(item):
        new_list.append(fn(item))

En utilisant une list comprehension :

new_list = [fn(item) for item in a_list
            if condition(item)]

Les listcomps sont claires et concises, directes. Vous pouvez avoir plusieurs boucles for et conditions if au sein d’une même listcomp, mais au-delà de deux ou trois, ou si les conditions sont complexes, je vous suggère d’utiliser l’habituelle boucle for. En appliquant le Zen de Python, utilisez la méthode la plus lisible.

Par exemple, la liste des carrés de 0 à 9 :

>>> [n ** 2 for n in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

La liste des nombres impairs au sein de la précédente liste :

>>> [n ** 2 for n in range(10) if n % 2]
[1, 9, 25, 49, 81]

Generator Expressions (1)

Faisons la somme des carrés des nombres inférieurs à 100 :

Avec une boucle :

total = 0
for num in range(1, 101):
    total += num * num

On peut aussi utiliser la fonction sum qui fait plus rapidement le travail pour nous en construisant la bonne séquence.

Avec une list comprehension :

total = sum([num * num for num in range(1, 101)])

Avec une generator expression :

total = sum(num * num for num in xrange(1, 101))

Les generator expressions (“genexps”) sont comme les list comprehensions, excepté dans leur calcul, les genexps sont fainéantes. Les listcomps calculent l’intégralité du résultat en une seule passe, pour le stocker dans une liste. Les generator expressions calculent une valeur à la fois, lorsqu’elle est nécessaire. C’est particulièrement utile lorsque la séquence est très longue lorsque la liste générée n’est qu’une étape intermédiaire et non le résultat final.

Dans ce cas, on est uniquement intéressé par la somme, on n’a pas besoin de la liste des résultats intermédiaires. On utilise xrange pour la même raison, ça génère les valeurs une par une.

Generator Expressions (2)

Par exemple si on doit faire la somme des carrés de plusieurs milliards d’entiers, on va arriver à une saturation de la mémoire avec une list comprehension, mais les generator expressions ne vont avoir aucun problème. Bon ça va prendre un certain temps par contre !

total = sum(num * num
            for num in xrange(1, 1000000000))

La différence de syntaxe est que les listcomps ont des crochets, alors que les genexps n’en ont pas. Les generator expressions nécessitent parfois des parenthèses par contre, vous devriez donc toujours les utiliser.

En bref :

  • Utilisez une list comprehension lorsque le résultat escompté est la liste.
  • Utilisez une generator expression lorsque la liste n’est qu’un résultat intermédiaire.

Voici un récent exemple de ce que j’ai vu au boulot.

On avait besoin d’un dictionnaire qui corresponde aux chiffres des mois (à la fois via des chaînes de caractères et via des integers) au code des mois pour un client. Cela peut être fait avec une ligne de code.

Ça fonctionne de la manière suivante :

  • La fonction de référence dict() prend en argument une liste de paires de clés/valeurs (2-tuples).
  • On a une liste des codes des mois (chaque code est une simple lettre, et une chaîne de caractères est aussi une simple liste de lettres). On parcours cette liste pour obtenir à la fois le code du mois et l’index.
  • Le nombre des mois commence à 1 mais Python commence l’indexation à 0, le nombre des mois correspond dont à index+1.
  • On veut avoir la correspondance à la fois avec les nombres et les chaînes de caractères. On peut utiliser les fonctions int() et str() pour ça et itérer dessus.

L’exemple en question :

 month_codes = dict((fn(i+1), code)
     for i, code in enumerate('FGHJKMNQUVXZ')
     for fn in (int, str))

Le résultat obtenu pour month_codes :

{ 1:  'F',  2:  'G',  3:  'H',  4:  'J', ...
 '1': 'F', '2': 'G', '3': 'H', '4': 'J', ...}

Ordonner

Il est très simple d’ordonner une liste en Python :

a_list.sort()

(Notez que la liste est ordonnée sur place, la liste originale est ordonnée et la fonction sort ne retourne pas une liste ou une copie.)

Mais que faire lorsque vous avec une liste de données à ordonner, mais quelle ne s’ordonne pas de manière naturelle ? Par exemple ordonner selon la première colonne, puis la quatrième.

On peut utiliser la fonction de référence sort avec une méthode définie par nos soins :

def custom_cmp(item1, item2):
    returm cmp((item1[1], item1[3]),
               (item2[1], item2[3]))

a_list.sort(custom_cmp)

Ça marche, mais c’est extrêmement lent pour les listes énormes.

Ordonner avec DSU

DSU = Decorate-Sort-Undecorate

Note: DSU n’est bien souvent plus nécessaire, cf. section suivante.

Au lieu de créer une fonction de comparaison personnalisée, on crée une liste intermédiaire qui va pourvoir être ordonnée naturellement :

# Decorate:
to_sort = [(item[1], item[3], item)
           for item in a_list]

# Sort:
to_sort.sort()

# Undecorate:
a_list = [item[-1] for item in to_sort]

La première ligne crée une liste contenant des tuples, une copie de la valeur à ordonner en premier argument, suivi de la valeur complète de la liste.

La seconde ligne ordonne grâce à la fonction Python, ce qui est très rapide.

La troisième ligne récupère la dernière valeur de la liste une fois ordonnée. Souvenez-vous, cette dernière valeur correspond à l’item complet. On n’utilise plus la partie ayant permis d’ordonner, elle a joué son rôle et n’est plus utile.

C’est un compromis espace mémoire + complexité vs. temps. Plus simple et rapide mais on est obligé de dupliquer la liste originale.

Ordonner avec keys

Python 2.4 a introduit un nouvel argument à la méthode sort des listes, “key”, qui permet de spécifier une fonction à un argument qui est utilisée pour comparer chaque élément d’une liste avec les autres. Par exemple :

def my_key(item):
    return (item[1], item[3])

to_sort.sort(key=my_key)

La fonction my_key va être appelée une fois par item de la liste to_sort.

Vous pouvez utiliser votre propre fonction ou utiliser une fonction existante qui ne prend qu’un seul argument :

  • str.lower pour ordonner alphabétiquement sans tenir compte de la casse.
  • len pour ordonner selon la taille des items (chaînes de caractères ou containers).
  • int ou float pour ordonner numériquement avec des valeurs qui sont des chaînes de caractères comme “2”, “123”, “35”.

Generators

On a déjà vu les generator expressions. On peut créer nos propres generators, comme des fonctions :

def my_range_generator(stop):
    value = 0
    while value < stop:
        yield value
        value += 1

for i in my_range_generator(10):
    do_something(i)

Le mot-clé yield transforme une fonction en generator. Lorsque vous appelez une fonction generator, au lieu d’exécuter le code directement, Python retourne un objet generator, qui est un itérateur. Il a une méthode next. Les boucles for appellent la méthode next de l’itérateur, jusqu’à ce qu’une exception du type StopIteration soit levée. Vous pouvez lever l’exception StopIteration explicitement ou de manière implicite comme dans le code ci-dessous.

Les générateurs peuvent simplifier la manière de gérer les séquences/itérateurs, car on n’a pas besoin de créer des listes intermédiaires. Ça ne génère qu’une valeur à la fois.

Voici comment la boucle for fonctionne réellement. Python analyse la séquence déclarée avec le mot-clé in. Si c’est un simple container (comme une liste, un tuple, un dictionnaire, un set ou un container défini par l’utilisateur) Python le converti en itérateur. Si c’est déjà un itérateur, Python ne fait rien.

Python appelle ensuite de manière itérative la méthode next de l’itérateur, assignant la valeur retournée au compteur de la boucle (i dans notre cas), et exécute le code indenté. C’est répété, encore et encore jusqu’à ce que StopIteration soit levée, ou qu’un break soit exécuté.

Une boucle for peut être dotée d’un else, au sein de laquelle le code est exécuté si rien ne s’est produit dans la boucle for, mais non après un break. Cette distinction permet de faire des choses élégantes. else est rarement utilisé avec la boucle for mais peut s’avérer très puissant lorsque la logique correspond à ce que vous souhaitez faire.

Par exemple, si on doit vérifier qu’une condition est toujours remplie par tous les items d’une liste :

for item in sequence:
    if condition(item):
        break
else:
    raise Exception('Condition not satisfied.')

Exemple de generator

Filtrer les colonnes vides à partir d’un fichier CSV (ou des items d’une liste) :

def filter_rows(row_iterator):
    for row in row_iterator:
        if row:
            yield row

data_file = open(path, 'rb')
irows = filter_rows(csv.reader(data_file))

Lire les lignes d’un fichier de données

datafile = open('datafile')
for line in datafile:
    do_something(line)

C’est possible car les fichiers sont dotés d’une méthode next, comme d’autres itérateurs : les listes, les tuples, les dictionnaires (pour leurs clés), les generators.

Il y a un piège ici : étant donnée la façon dont sont mises en cache les données, vous ne pouvez pas utiliser à la fois les méthodes .next et .read* à moins que vous n’utilisez Python 2.5+.

EAFP vs. LBYL

Il est plus facile de demander le pardon que la permission (EAFP) vs. analyser avant d’échouer (LBYL). Généralement EAFP est préféré, mais pas toujours.

  • Duck typing Si ça marche comme un canard, parle comme un canard et ressemble à un canard : c’est un canard.
  • Exceptions Essayez de forcer le type si un objet doit être d’un type particulier. Si x doit être une chaîne de caractères pour que votre code fonctionne, pourquoi ne pas appeler str(x) au lieu d’essayer quelque chose commeisinstance(x, str)`.

Exemple EAFP try/except

Vous pouvez encapsuler vos exceptions dans un bloc try/except pour pour récupérer les erreurs et vous allez probablement arriver à une solution qui est beaucoup plus générale que si vous aviez essayer d’anticiper chaque cas.

try:
    return str(x)
except TypeError:
    ...

Note: Spécifiez toujours l’exception à attraper. N’utilisez jamais except tout seul. Sinon except va cacher d’autres exceptions qui risquent d’être levées rendant votre code très difficile à debugger.

Importer

from module import *

Vous avez probablement déjà vu cette manière de faires des imports avec une “étoile”. Vous l’appréciez peut-être. Ne l’utilisez pas.

Pour paraphraser un exemple très connu :

LUKE: Est-ce que from module import * est meilleur que des imports explicites ? YODA: Non, pas meilleur. Plus rapide, plus simple, plus séduisant. LUKE: Mais comment saurais-je pourquoi les imports explicites sont meilleurs que les formes étoilées ? YODA: Tu sauras lorsque ton code dans 6 mois tu essayeras de lire.

Les imports étoilés sont le mauvais côté de la Force en Python.

Les imports de type from module import * polluent votre espace de nom. Vous allez avoir des choses que vous n’attendiez pas. Vous pouvez avoir des conflits avec les noms que vous avez défini localement. Vous n’allez plus savoir d’où viennent certains noms. Bien que ce soit un raccourci pratique, ça ne doit pas arriver en production.

Morale : n’utilisez pas d’imports étoilés !

Il est bien meilleur de référencer les noms à partir de leurs modules :

import module
module.name

importer un module avec un nom plus court si nécessaire (avec alias) :

import long_module_name as mod
mod.name

ou importer juste les noms dont vous avez besoin de manière explicite :

from module import name
name

Notez qu’il est nécessaire d’utiliser “reload()” sur un module lorsque vous utilisez le prompt interactif si vous éditez celui-ci.

Modules et scripts

Pour faire à la fois un module importable et un script exécutable :

if __name__ == '__main__':
    # script code here

Lorsqu’il est importé, un attribut__name__ est setté, correspondant au nom du fichier du module, sans “.py”. Le code ci-dessus ne va donc pas être lancé lors d’un import. Lorsqu’il est lancé comme un script, l’attribut __name__ est setté à “main” et le script va être exécuté.

Excepté pour certains cas spéciaux, vous ne devriez placer aucun code important au plus haut niveau. Placez votre code dans des fonctions, classes, méthodes et protégez le avec if __name__ == '__main__'.

Structure d’un module

"""module docstring"""

# imports
# constants
# exception classes
# interface functions
# classes
# internal functions & classes

def main(...):
    ...

if __name__ == '__main__':
    status = main()
    sys.exit(status)

C’est la façon dont un module devrait être structuré.

Utilisation de la ligne de commande

Exemple (Note du traducteur : j’ai déjà parlé de ça aussi) :

#!/usr/bin/env python

"""
Module docstring.
"""

import sys
import optparse

def process_command_line(argv):
    """
    Return a 2-tuple: (settings object, args list).
    `argv` is a list of arguments, or `None` for ``sys.argv[1:]``.
    """
    if argv is None:
        argv = sys.argv[1:]

    # initialize the parser object:
    parser = optparse.OptionParser(
        formatter=optparse.TitledHelpFormatter(width=78),
        add_help_option=None)

    # define options here:
    parser.add_option(      # customized description; put --help last
        '-h', '--help', action='help',
        help='Show this help message and exit.')

    settings, args = parser.parse_args(argv)

    # check number of arguments, verify values, etc.:
    if args:
        parser.error('program takes no command-line arguments; '
                     '"%s" ignored.' % (args,))

    # further process settings & args if necessary

    return settings, args

def main(argv=None):
    settings, args = process_command_line(argv)
    # application code here, like:
    # run(settings, args)
    return 0        # success

if __name__ == '__main__':
    status = main()
    sys.exit(status)

Packages

package/
    __init__.py
    module1.py
    subpackage/
        __init__.py
        module2.py
  • Utilisés pour organiser un projet.
  • Réduisent le nombre d’entrées lors du chargement.
  • Réduisent les conflits en cas d’imports.

Exemple :

import package.module1
from package.subpackage import module2
from package.subpackage.module2 import name

En Python 2.5 on a maintenant les imports absolus et relatifs via un import du futur :

from __future__ import absolute_import

Je n’ai pas encore eu l’occasion de tester ça moi-même, on va donc couper court à toute discussion à ce sujet.

Simple is Better Than Complex

Débugger est deux fois plus difficile que d’écrire du code en premier jet. De plus, si vous écrivez le code aussi intelligemment que possible, vous êtes, par définition, pas assez intelligent pour le débugger.

-- Brian W. Kernighan, co-auteur de The C Programming Language et le “K” dans “AWK”

En d’autres termes, gardez vos programmes simples !

Ne réinventez pas la roue

Avant d’écrire une seule ligne de code,

  • Vérifiez que cela n’est pas dans la bibliothèque standard de Python.
  • Vérifiez que cela n’est pas dans le Python Package Index (the “Cheese Shop”)
  • Cherchez sur le web. Google is your friend.

Retour du traducteur

Pour terminer, et si vous souhaitez aller plus loin, une excellente présentation sur les générateurs que je n’aurais malheureusement pas le temps de traduire permet d’envisager la programmation Python d’une manière tout à fait différente. Elle est orientée administration système mais elle peut vraiment être appliquée à de nombreux cas. Et si vous voulez comprendre l’intérêt des décorateurs, un bon exemple vaut mieux que tous les discours. Mangez du Python, c’est bon pour la santé !