Style de code

Si vous demandez aux programmeurs Python ce qu’ils aiment le plus à propos de Python, ils citeront souvent sa grande lisibilité. En effet, un haut niveau de lisibilité est au cœur de la conception du langage Python, après le fait reconnu que le code est lu beaucoup plus souvent que ce qui est écrit.

Une raison de la haute lisibilité du code Python est son jeu relativement complet d’instructions de style de code et ses idiomes “Pythoniques”.

Quand un développeur Python vétéran (un Pythoniste) appelle des portions de code non “Pythoniques”, il veut dire généralement que ces lignes de code ne suivent pas les instructions communes et ne parviennent pas à exprimer leur intention dans ce qui est considéré comme la meilleure façon (entendez: la manière la plus lisible).

Dans certains cas limites, aucune meilleure pratique n’a été convenue sur la façon d’exprimer une intention en code Python, mais ces cas sont rares.

Concepts généraux

Code explicite

Alors que toute sorte de magie noire est possible avec Python, la manière la plus explicite et directe est préférable.

Mauvais

def make_complex(*args):
    x, y = args
    return dict(**locals())

Bon

def make_complex(x, y):
    return {'x': x, 'y': y}

Dans le bon code ci-dessus, x et y sont explicitement reçus de l’appelant, et un dictionnaire explicite est retourné. Le développeur utilisant cette fonction sait exactement ce qu’il faut faire en lisant la première et la dernière ligne, ce qui n’est pas le cas avec le mauvais exemple.

Une déclaration par ligne

Bien que certaines déclarations composées comme les compréhensions de liste soient autorisées et appréciées pour leur brièveté et leur expressivité, c’est une mauvaise pratique d’avoir deux déclarations disjointes sur la même ligne de code.

Mauvais

print 'one'; print 'two'

if x == 1: print 'one'

if <complex comparison> and <other complex comparison>:
    # do something

Bon

print 'one'
print 'two'

if x == 1:
    print 'one'

cond1 = <complex comparison>
cond2 = <other complex comparison>
if cond1 and cond2:
    # do something

Arguments de fonction

Les arguments peuvent être passés aux fonctions de quatre manières différentes.

  1. Les arguments positionnels sont obligatoires et n’ont pas de valeurs par défaut. Ils sont la forme la plus simple des arguments et ils peuvent être utilisés pour les quelques arguments de fonction qui sont partie intégrante de la signification de la fonction et leur ordre est naturel. Par exemple, dans send(message, recipient) ou point(x, y) l’utilisateur de la fonction n’a pas de difficulté à se souvenir que ces deux fonctions nécessitent deux arguments, et dans quel ordre.

Dans ces deux cas, il est possible d’utiliser des noms d’argument lors de l’appel des fonctions et, ce faisant, il est possible de changer l’ordre des arguments, appelant par exemple send(recipient='World', message='Hello') et point(y=2, x=1), mais cela réduit la lisibilité et est inutilement verbeux, par rapport aux appels plus simples à send('Hello', 'World') et point(1, 2).

  1. Les arguments nommés ne sont pas obligatoires et ont des valeurs par défaut. Ils sont souvent utilisés pour les paramètres facultatifs envoyés à la fonction. Quand une fonction a plus de deux ou trois paramètres positionnels, sa signature est plus difficile à retenir et l’utilisation d’arguments nommés avec des valeurs par défaut est utile. Par exemple, une fonction send plus complète pourrait être définie comme send(message, to, cc=None, bcc=None). Ici cc et bcc sont facultatifs, et sont évalués à None quand ils ne reçoivent pas une autre valeur.

L’appel d’une fonction avec des arguments nommés peut être fait de plusieurs façons en Python. Par exemple, il est possible de suivre l’ordre des arguments dans la définition sans nommer explicitement les arguments, comme dans send('Hello', 'World', 'Cthulhu', 'God'), envoyant une copie carbone invisible à God. Il serait également possible de nommer des arguments dans un autre ordre, comme dans send('Hello again', 'World', bcc='God', cc='Cthulhu'). Ces deux possibilités sont mieux évitées sans aucune vraie raison de ne pas suivre la syntaxe qui est le plus proche de la définition de la fonction: send('Hello', 'World', cc='Cthulhu', bcc='God').

Comme note à garder de côté, en suivant le principe YAGNI, il est souvent plus difficile d’enlever un argument optionnel (et sa logique dans la fonction) qui a été ajouté “juste au cas où” et n’est apparemment jamais utilisé, que d’ajouter un nouvel argument optionnel et sa logique en cas de besoin.

  1. La liste d’arguments arbitraires est la troisième façon de passer des arguments à une fonction. Si l’intention de la fonction est mieux exprimée par une signature avec un nombre extensible d’arguments positionnels, elle peut être définie avec les constructions args. Dans le corps de la fonction, args sera un tuple de tous les arguments positionnels restants. Par exemple, send(message, *args) peut être appelé avec chaque destinataire comme argument: send('Hello', 'God', 'Mom', 'Cthulhu'), et dans le corps de la fonction args sera égal à ('God', 'Mom', 'Cthulhu').

Cependant, cette construction présente des inconvénients et doit être utilisée avec prudence. Si une fonction reçoit une liste d’arguments de même nature, il est souvent plus clair de la définir comme une fonction d’un seul argument, cet argument étant une liste ou n’importe quelle séquence. Ici, si send a plusieurs destinataires, il est préférable de la définir explicitement: send(message, recipients) et de l’appeler avec send('Hello', ['God', 'Mom', 'Cthulhu']). De cette façon, l’utilisateur de la fonction peut manipuler la liste des destinataires sous forme de liste à l’avance, et cela ouvre la possibilité de passer un ordre quelconque, y compris les itérateurs, qui ne peuvent être unpacked comme d’autres séquences.

  1. Le dictionnaire d’arguments mot-clé arbitraire est le dernier moyen de passer des arguments aux fonctions. Si la fonction nécessite une série indéterminée d’arguments nommés, il est possible d’utiliser la construction **kwargs. Dans le corps de la fonction, kwargs sera un dictionnaire de tous les arguments nommés passés qui n’ont pas été récupérés par d’autres arguments mot-clés dans la signature de la fonction.

La même prudence que dans le cas de liste d’arguments arbitraires est nécessaire, pour des raisons similaires: ces techniques puissantes doivent être utilisés quand il y a une nécessité avérée de les utiliser, et elles ne devraient pas être utilisées si la construction simple et plus claire est suffisante pour exprimer l’intention de la fonction.

Il appartient au programmeur d’écrire la fonction pour déterminer quels arguments sont des arguments positionnels et qui sont des arguments optionnels mots-clés, et de décider d’utiliser ou non les techniques avancées de passage d’arguments arbitraires. Si le conseil ci-dessus est suivi à bon escient, il est possible et agréable d’écrire des fonctions Python qui sont:

  • faciles à lire (le nom et les arguments n’ont pas besoin d’explications)

  • faciles à changer (l’ajout d’un nouveau mot-clé en argument ne casse pas les autres parties du code)

Éviter la baguette magique

Un outil puissant pour les hackers, Python vient avec un jeu très riche de hooks et d’outils vous permettant de faire presque tout sorte de trucs délicats. Par exemple, il est possible de faire chacun des éléments suivants:

  • changer comment les objets sont créés et instanciés

  • changer comment l’interpréteur Python importe les modules

  • il est même possible (et recommandé si nécessaire) pour intégrer des routines C dans Python.

Cependant, toutes ces options présentent de nombreux inconvénients et il est toujours préférable d’utiliser le moyen le plus simple pour atteindre votre objectif. Le principal inconvénient est que la lisibilité souffre beaucoup lors de l’utilisation de ces constructions. De nombreux outils d’analyse de code, comme pylint ou pyflakes, ne pourront pas analyser ce code “magique”.

Nous considérons qu’un développeur Python devrait connaître ces possibilités presque infinies, car cela inspire la confiance qu’aucun problème infranchissable ne sera sur le chemin. Cependant, savoir comment et surtout quand ne pas les utiliser est très important.

Comme un maître de kung-fu, un Pythoniste sait comment tuer avec un seul doigt, et ne jamais le faire pour de vrai.

Nous sommes tous des utilisateurs responsables

Comme on le voit ci-dessus, Python permet de nombreuses astuces, et certains d’entre elles sont potentiellement dangereuses. Un bon exemple est que tout code client peut surcharger les propriétés et les méthodes d’un objet: il n’y a pas mot-clé “private” en Python. Cette philosophie, très différente des langages très défensifs comme Java, qui donnent beaucoup de mécanismes pour empêcher toute utilisation abusive, est exprimée par le dicton: “Nous sommes tous les utilisateurs responsables”.

Cela ne veut pas dire que, par exemple, que des propriétés ne sont pas considérées comme privées, et qu’aucune encapsulation appropriée n’est possible en Python. Au contraire, au lieu de compter sur les murs de béton érigés par les développeurs entre leur code et les autres, la communauté Python préfère compter sur un ensemble de conventions indiquant que ces éléments ne doivent pas être directement accessibles.

La convention principale pour les propriétés privées et les détails d’implémentation est de préfixer toutes les “caractéristiques internes” avec un tiret bas. Si le code client enfreint cette règle et accède à ces éléments marqués, tous les mauvais comportements ou problèmes rencontrés si le code est modifié est de la responsabilité du code client.

L’utilisation de cette convention généreusement est encouragée: toute méthode ou une propriété qui ne sont pas destinées à être utilisées par le code client doivent être préfixées avec un tiret bas. Cela permettra de garantir une meilleure séparation des tâches et une modification plus facile du code existant; il sera toujours possible d’exposer une propriété privée, mais rendre une propriété publique privée pourrait être une opération beaucoup plus difficile.

Valeurs retournées

Quand une fonction croît en complexité, il n’est pas rare d’utiliser des instructions de retour multiples à l’intérieur du corps de la fonction. Cependant, afin de maintenir une intention claire et un niveau de lisibilité durable, il est préférable d’éviter de retourner des valeurs significatives à partir de nombreux points de sortie dans le corps.

Il existe deux principaux cas pour retourner des valeurs dans une fonction: le résultat du retour de la fonction quand il a été traité normalement, et les cas d’erreur indiquant un paramètre d’entrée erroné ou toute autre raison pour la fonction de ne pas être capable de compléter son calcul ou sa tâche.

Si vous ne souhaitez pas de lever des exceptions pour le second cas, puis retourner une valeur, comme None ou False, indiquer que la fonction pourrait ne pas fonctionner correctement pourrait être nécessaire. Dans ce cas, il est préférable de la retourner aussitôt que le contexte incorrect a été détecté. Cela aidera à aplatir la structure de la fonction: tout le code après l’instruction retour-parce-que-erreur peut assumer que la condition est remplie pour continuer à calculer le résultat principal de la fonction. Avoir de telles multiples déclarations de retour est souvent nécessaire.

Cependant, lorsqu’une fonction a plusieurs principaux points de sortie pour son fonctionnement normal, il devient difficile de débugguer le résultat retourné, donc il peut être préférable de garder un seul point de sortie. Cela permettra également d’aider à refactoriser quelques chemins dans le code, et les points de sortie multiples sont une indication probable qu’un tel refactoring est nécessaire.

def complex_function(a, b, c):
    if not a:
        return None  # Raising an exception might be better
    if not b:
        return None  # Raising an exception might be better
    # Some complex code trying to compute x from a, b and c
    # Resist temptation to return x if succeeded
    if not x:
        # Some Plan-B computation of x
    return x  # One single exit point for the returned value x will help
              # when maintaining the code.

Idiomes

Un idiome de langage, dit simplement, est un moyen d’écrire du code. La notion d’idiomes de programmation est discutée amplement sur c2 et sur Stack Overflow.

Du code Python idiomatique est souvent désigné comme étant Pythonique.

Bien qu’il y ait habituellement une — et de préférence une seule — manière évidente de le faire; la façon d’écrire du code Python idiomatique peut être non évidente pour les débutants Python. Donc, les bons idiomes doivent être consciemment acquis.

Certains idiomes Python communs suivent:

Unpacking

Si vous connaissez la longueur d’une liste ou d’un tuple, vous pouvez attribuer des noms à ses éléments avec l’unpacking. Par exemple, étant donné que enumerate() fournira un tuple de deux éléments pour chaque élément dans la liste:

for index, item in enumerate(some_list):
    # do something with index and item

Vous pouvez l’utiliser pour intervertir des variables ainsi:

a, b = b, a

L’unpacking imbriqué marche aussi:

a, (b, c) = 1, (2, 3)

En Python 3, une nouvelle méthode d’unpacking étendue a été introduite par la PEP 3132:

a, *rest = [1, 2, 3]
# a = 1, rest = [2, 3]
a, *middle, c = [1, 2, 3, 4]
# a = 1, middle = [2, 3], c = 4

Créer une variable ignorée

Si vous avez besoin d’assigner quelque chose (par exemple, dans Unpacking) mais que vous n’avez pas besoin de cette variable, utilisez __:

filename = 'foobar.txt'
basename, __, ext = filename.rpartition('.')

Note

Beaucoup de guides de style Python recommandent l’utilisation d’un seul tiret bas “_” pour les variables jetables plutôt que les tirets bas doubles “__” recommandés ici. Le problème est que “_” est couramment utilisé comme un alias pour la fonction gettext(), et est également utilisé dans l’invite interactive pour garder la valeur de la dernière opération. L’utilisation d’un tiret bas double est à la place est tout aussi clair et presque aussi pratique, et élimine le risque d’interférer accidentellement avec l’un de ces autres cas d’utilisation.

Créer un liste de longueur N de la même chose

Utilisez l’opérateur de liste * de Python:

four_nones = [None] * 4

Créez un liste de longueur N de listes

parce que les listes sont mutables, l’opérateur * (comme ci-dessus) créera une liste de N références vers la même liste, ce qui est probablement pas ce que vous voulez. A la place, utilisez une compréhension de liste:

four_lists = [[] for __ in xrange(4)]

Note: utilisez range() à la place de xrange() en Python 3

Créer une chaîne depuis une liste

Un idiome commun pour la création de chaînes est d’utiliser str.join() sur une chaîne vide.

letters = ['s', 'p', 'a', 'm']
word = ''.join(letters)

Cela va définir la valeur de la variable word à ‘spam’. Cet idiome peut être appliqué à des listes et des tuples.

Rechercher un élément dans une collection

Parfois, nous devons chercher à travers une collection de choses. Regardons deux options: lists et sets.

Prenez le code suivant pour exemple:

s = set(['s', 'p', 'a', 'm'])
l = ['s', 'p', 'a', 'm']

def lookup_set(s):
    return 's' in s

def lookup_list(l):
    return 's' in l

Même si les deux fonctions semblent identiques, parce que lookup_set utilise le fait que les sets en Python sont des tables de hashage, les performances de recherche entre les deux sont très différentes. Pour déterminer si un élément se trouve dans une liste, Python devra parcourir chaque élément jusqu’à ce qu’il trouve un élément correspondant. Cela prend du temps, surtout pour de longues listes. Dans un set, d’autre part, le hachage de l’élément dira à Python où dans le set chercher un élément correspondant. En conséquence, la recherche peut être faite rapidement, même si le set est grand. La recherche dans les dictionnaires fonctionne de la même façon. Pour plus d’informations, voir cette page StackOverflow. Pour plus d’informations sur la quantité de temps nécessaire pour les différentes opérations courantes pour chacune de ces structures de données, voir cette page.

En raison de ces différences de performance, c’est souvent une bonne idée d’utiliser des sets ou des dictionnaires au lieu de listes dans les cas où:

  • La collection contiendra un grand nombre d’éléments

  • Vous rechercherez de manière répétitive les éléments dans la collection

  • Vous n’avez pas pas d’éléments dupliqués.

Pour les petites collections, ou les collections que vous n’avez pas fréquemment à rechercher, le temps additionnel et la mémoire requise pour configurer la table de hashage seront souvent plus longs que le temps gagné grâce à l’amélioration de la vitesse de recherche.

Le Zen de Python

Aussi connu comme PEP 20, les principes directeurs pour la conception de Python.

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Pour quelques exemples de bons styles Python, voir ces diapositives d’un groupe d’utilisateurs Python.

PEP 8

PEP 8 est le guide de fait du style de code pour Python. Une version de haute qualité, facile à lire de la PEP 8 est également disponible sur pep8.org.

C’est une lecture fortement recommandée. La communauté Python entière fait de son mieux pour adhérer aux instructions énoncées dans le présent document. Certains projets peuvent osciller autour d’elles de temps à autre, tandis que d’autres peuvent modifier ses recommandations.

Cela étant dit, conformer votre code Python à PEP 8 est généralement une bonne idée et contribue à rendre le code plus consistant lorsque vous travaillez sur des projets avec d’autres développeurs. Il existe un programme en ligne de commande, pep8, qui peut vérifier la conformité de votre code. Installez-le en exécutant la commande suivante dans votre terminal:

$ pip install pep8

Ensuite, exécutez-le sur un fichier ou une série de fichiers pour avoir un rapport de toutes violations.

$ pep8 optparse.py
optparse.py:69:11: E401 multiple imports on one line
optparse.py:77:1: E302 expected 2 blank lines, found 1
optparse.py:88:5: E301 expected 1 blank line, found 0
optparse.py:222:34: W602 deprecated form of raising exception
optparse.py:347:31: E211 whitespace before '('
optparse.py:357:17: E201 whitespace after '{'
optparse.py:472:29: E221 multiple spaces before operator
optparse.py:544:21: W601 .has_key() is deprecated, use 'in'

Le programme autopep8 peut être utilisé pour reformater automatiquement le code dans le style PEP 8. Installez le programme avec:

$ pip install autopep8

Utilisez-le pour formater un fichier sur place avec:

$ autopep8 --in-place optparse.py

Exclure l’option --in-place va mener le programme à renvoyer en sortie le code modifié directement dans la console pour examen. L’option --aggressive effectuera des changements plus importants et peut être appliquée à plusieurs reprises pour plus d’effet.

Conventions

Voici quelques conventions que vous devriez suivre pour rendre votre code plus facile à lire.

Vérifier sir la variable est égale à une constante

Vous n’avez pas besoin de comparer explicitement une valeur à True ou None, ou 0 - vous pouvez simplement l’ajouter à l’instruction if. Voir le Test des valeurs à True pour une liste de ce qui est considéré comme False.

Mauvais:

if attr == True:
    print 'True!'

if attr == None:
    print 'attr is None!'

Bon:

# Just check the value
if attr:
    print 'attr is truthy!'

# or check for the opposite
if not attr:
    print 'attr is falsey!'

# or, since None is considered false, explicitly check for it
if attr is None:
    print 'attr is None!'

Accéder à un élément de dictionnaire

N’utilisez pas la méthode dict.has_key(). A la place,utilisez la syntaxe x in d, ou passez un argument par défaut à dict.get().

Mauvais:

d = {'hello': 'world'}
if d.has_key('hello'):
    print d['hello']    # prints 'world'
else:
    print 'default_value'

Bon:

d = {'hello': 'world'}

print d.get('hello', 'default_value') # prints 'world'
print d.get('thingy', 'default_value') # prints 'default_value'

# Or:
if 'hello' in d:
    print d['hello']

Façons courtes de manipuler des listes

Les listes en compréhensions fournissent un manière puissante et concise de travailler avec les listes. En outre, les fonctions map() et filter() peuvent effectuer des opérations sur des listes en utilisant une syntaxe différente et plus concise.

Mauvais:

# Filter elements greater than 4
a = [3, 4, 5]
b = []
for i in a:
    if i > 4:
        b.append(i)

Bon:

a = [3, 4, 5]
b = [i for i in a if i > 4]
# Or:
b = filter(lambda x: x > 4, a)

Mauvais:

# Add three to all list members.
a = [3, 4, 5]
for i in range(len(a)):
    a[i] += 3

Bon:

a = [3, 4, 5]
a = [i + 3 for i in a]
# Or:
a = map(lambda i: i + 3, a)

Utilisez enumerate() tient un compte de votre position dans la liste.

a = [3, 4, 5]
for i, item in enumerate(a):
    print i, item
# prints
# 0 3
# 1 4
# 2 5

La fonction enumerate() a une meilleure lisibilité que la gestion d’un compteur manuellement. De plus, elle est plus optimisés pour les itérateurs.

Lire depuis un fichier

Utilisez la syntaxe with open pour lire depuis des fichiers. Cela fermera automatiquement les fichiers pour vous.

Mauvais:

f = open('file.txt')
a = f.read()
print a
f.close()

Bon:

with open('file.txt') as f:
    for line in f:
        print line

La déclaration with est meilleure parce qu’elle assure que vous fermez toujours le fichier, même si une exception est levée à l’intérieur du block with.

Continuations de ligne

Quand une ligne logique de code est plus longue que la limite acceptée, vous devez la diviser sur plusieurs lignes physiques. L’interpréteur Python rejoindra lignes consécutives si le dernier caractère de la ligne est une barre oblique inverse. Ceci est utile dans certains cas, mais doit généralement être évitée en raison de sa fragilité: un espace blanc ajouté à la fin de la ligne, après la barre oblique inverse, va casser le code et peut avoir des résultats inattendus.

Une meilleure solution est d’utiliser des parenthèses autour de vos éléments. Avec une parenthèse non fermée laissée à la fin d’une ligne, l’interpréteur Python joindra la ligne suivante jusqu’à ce que les parenthèses soient fermées. Le même comportement est valable pour des accolades et des crochets.

Mauvais:

my_very_big_string = """For a long time I used to go to bed early. Sometimes, \
    when I had put out my candle, my eyes would close so quickly that I had not even \
    time to say “I’m going to sleep.”"""

from some.deep.module.inside.a.module import a_nice_function, another_nice_function, \
    yet_another_nice_function

Bon:

my_very_big_string = (
    "For a long time I used to go to bed early. Sometimes, "
    "when I had put out my candle, my eyes would close so quickly "
    "that I had not even time to say “I’m going to sleep.”"
)

from some.deep.module.inside.a.module import (
    a_nice_function, another_nice_function, yet_another_nice_function)

Cependant, le plus souvent, avoir à couper une longue ligne logique est un signe que vous essayez de faire trop de choses en même temps, ce qui peut gêner la lisibilité.