Bien nommer ses variables en Python


There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

Utiliser des bons noms est le geste de documentation le plus important d’un code. Je ne parle pas de bien formater le nom des variables. Pour ça, il y a le PEP8 et ce qu’il recommande tient en 3 lignes :

  • Le nom des classes en CamelCase
  • Les (pseudo) constantes en UPPER_CASE
  • Le reste en snake_case

C’est tout.

Non, je parle de choisir un nom adapté et descriptif.

Le post est long, et vous savez que quand le post est long, je vous mets une musique d’attente.

Explicit is better than implicit

En Python, il n’y a pas de déclaration de type. Le nom d’une variable a donc d’autant plus d’importance pour expliquer ce qu’il y a dedans.

Prenez cet exemple :

best = []
for k, v in data.items():
    if v > top:
        best.append(k)

Quand on lit ce bout de code, on se demande :

  • Best ? Mais best en quoi ?
  • Que contient data ?
  • Quel est la nature de top ?

Maintenant, avec des noms explicites :

best_players = []
for player, score in data.items():
    if score > top_score :
        best_players.append(player)

On comprend tout de suite de quoi il est question. L’algo n’a pas changé, seul le nommage a changé.

Et si on passe à une écriture plus compacte, le gain est encore plus net :

best = [k for k, v in data.items() if k > top]

VS

best_players = [player for player, score in data.items() if score > top_score]

Parfois, on veut la concision, mais faute de nommage, on doit se retourner vers les commentaires :

# Get players with the best scores
best = [k for k, v in data.items() if k > top]

Néanmoins, si on doit faire le choix entre commenter et bien nommer, le nommage doit avoir priorité. Le commentaire est important, mais c’est le dernier recours d’un code qui n’est pas explicite. Avoir un code clair doit être l’objectif premier. Ensuite, seulement, on le commente (abondamment toutefois, faut pas être radin).

Si vous avez suivi, je nomme mes variables en fonction de leur nature, pas leur type. On peut utiliser les règles de l’orthographe pour encore plus de précision, par exemple le pluriel.

fruits = ["kiwi", "banane", 'poire']
for fruit in fruits:
    print(fruit)

J’utilise le pluriel pour une liste de données, qui va donc potentiellement contenir plusieurs fruits. Mais j’utilise un singulier dans la boucle pour le fruit en court.

L’utilisation d’adjectifs est aussi bienvenue :

fruits peut devenir, après un traitement filtered_fruits, indiquant que la liste a subi un filtrage. Les mots en “ed” en anglais aident beaucoup à la qualification.

On évite au maximum les variables courtes. Certains cas sont néanmoins tolérés. Le premier est l’utilisation de i, x, yet z pour des indices.

for i, fruit in enumerate(fruits):
    # faire un truc

Les indices sont quelque chose de tellement courant en informatique qu’on ne va pas se gaver à l’appeler “indice” à chaque fois.

Le second est dans le cadre scientifique. On a souvent des variables pour un algo, des coordonnées, des valeurs géométriques ou mathématiques, qui n’ont pas de dénomination. Dans ce cas, inutile d’essayer d’inventer une nomenclature tordue. Exemple typique, l’algo pour pondre un MD5. Mais il faut compenser par des commentaires, sinon on s’y perd.

Il ne faut pas avoir peur des noms longs. Si, j’ai un jeu de données, que je filtre plusieurs fois, il est de bon ton de distinguer les différents jeux avec des noms détaillés :

sample = range(10)
squares = [x * x for x in sample]
even_squares = [x for x in squares if x % 2 == 0]
even_squares_tail = even_squares[-3:]

Faire des noms de plus de 10 caractères n’est pas sale. On est pas sur un tableau des scores d’une borne d’arcade des années 80.

J’utilise bien entendu des noms en anglais, ce qui est toujours préférable, mais si vous devez en mettre en fr, évitez à tout prix les accents malgré la possibilité de les utiliser en Python 3.

Conventions

Il existe quelques noms qui sont toujours utilisés de la même façon en Python.

self et cls sont les plus connus, en j’en parle déjà dans le dossier sur la POO.

Il y a args et kwargs, qu’on utilise avec l’opérateur splat, dont je parle ici.

Et puis il y en a de plus discrets.

_ est utilisé pour une variable qu’on ignore. Certaines opérations, comme l’unpacking, supposent la création de plusieurs variables. Si on n’est pas intéressé par l’une d’elles, on peut le signaler. Par exemple, l’ORM django permet d’obtenir un objet, et si il n’existe pas, de le créer. Cette fonction retourne un tuble (objet, bool), l’élément indiquant si l’objet a été créé ou non. Si cette information nous intéresse :

user, created = User.objects.get_or_create(username="sam")

Si cette information ne nous intéresse pas :

user, _ = User.objects.get_or_create(username="sam")

Ainsi le lecteur saura qu’il peut ignorer cette variable quand il parcourt le code.

Il y a aussi les alias. On ne peut pas utiliser certains noms comme list ou id qui sont des fonctions existantes en Python.

On s’arrange généralement en trouvant un synonyme, mais si ce n’est pas pratique, on change une lettre. list devient lst (rappelez vous que list est un nom déjà assez pourri, nommez plutôt le contenu), class devient klass, dict devient dct, etc.

Si on ne peut pas le faire, la convention est de rajouter un underscore à la fin du nom : id devient id_, max devient max_… Mais faites l’effort, avant, de chercher un synonyme. Je vois trop souvent des from_/to alors que certains contextes permettent parfaitement de les nommer start/end ou source/destination.

A ne pas confondre avec l’underscore AVANT le nom, qui est une convention pour dire qu’une variable ne fait pas partie de l’API publique.

Ce sont des béquilles, le choix d’un nom judicieux et clair est toujours préférable, mais ce sont des béquilles utiles.

Savoir quand nommer

Au-delà de donner un bon nom, il y a le fait de choisir quand il faut nommer, et quand il faut éviter de le faire.

Si seul le résultat final d’un traitement m’intéresse, alors, il vaut mieux utiliser une seule variable et mettre le nouveau résultat dedans à chaque fois :

fstab = [line.strip() for line in open('/etc/fstab') if line]
fstab = [line.lower() for line in fstab if not line.startswith('#')]
fstab = [line.split()[:3] for line in fstab]

il faut aussi savoir quand ne pas du tout créer une variable :

row = line.strip().split()
for col in row:
    # do something

ici, la variable intermédiaire est inutile :

for col in line.strip().split():
    # do something

L’inverse est aussi vrai :

for col in [int(x) for x in line.strip().split()]:
    # do something

La ligne devient beaucoup trop complexe, et ajouter une variable intermédiaire avec un bon nom va améliorer la lisibilité du programme :

numeric_col = [int(x) for x in line.strip().split()]
for col in numeric_col:
    # do something

On pourrait croire que je précise le type ici en utilisant “numeric”, mais je n’ai pas utilisé integer ou float. J’ai précisé la nature : des colonnes numériques. Il se trouve que pour des données aussi brutes, la nature se rapproche du type.

Si vous êtes du genre à utiliser des lambdas, cela s’applique aussi à vous.

Pour quelque chose de simple, une lambda inline est très lisible :

sorted(scores.items(), key=lambda score: score[1])

Mais pour quelque chose de complexe, une fonction complète est bien plus adaptée :

def calculate_rank(score):
    return sum(goals for sort, goal in score[1] if sort == 'A')
 
sorted(scores.items(), key=calculate_rank)

Plutôt qu’un horrible :

sorted(scores.items(), key=lambda x: sum(g for s, g in x[1] if s == 'A'))

Pourquoi je parle de lambda alors qu’on est sur du nommage ? Parce qu’une lambda est anonyme, alors qu’une fonction normale a un nom. Et ce nom exprime l’action de la fonction. Il documente.

Habitudes stylistiques

Ces règles là ne sont pas officielles, mais j’ai pu les constater dans nombre de bons codes.

La nom d’une fonction/méthode est aussi important, sinon plus, que le nom d’une variable. Il n’est pas rare que j’écrive des fonctions avec des noms bien dodus comme :

def get_last_downloaded_shemale_vids()

Si on oublie la docstring, on a déjà une bonne idée de ce que cette fonction fait. Cela n’empêche pas de docstringuer quand même pour annoncer des subtilités sur les types, les potentiels side effects, des choses à savoir sur le temps d’exécution, le format, les perfs, etc.

Mais il arrive souvent qu’une fonction ne fasse pas quelque chose d’aussi concret. Prenez par exemple une fonction dont le but est de sluggifier les strings d’une list.

Si la fonction transforme la liste, on va utiliser un verbe dans le nom :

slugify_items(data)

Si par contre la fonction retourne une liste avec les éléments modifiés, on va utiliser le participe passé :

data = slugified_items(data)

C’est subtil, mais la sémantique est différente. Dans le premier cas, on s’attend à un effet de bord. Dans le second cas, on s’attend à une nouvelle liste, ou comme souvent en Python, à un générateur.

Quand on a affaire à une méthode en Python, on utilise rarement le préfixe get_. Personnellement je l’utilise parfois pour des actions complexes, ou des méthodes de classe.

Mais généralement, on préférera utiliser directement le nom de ce qu’on veut récupérer. Exemple :

comments = blog_post.get_comments(spam=False) # NON
comments = blog_post.comments(spam=False) # Oui

Si on n’a pas besoin de passer de paramètre, alors une property est plus appropriée :

comments = blog_post.comments # Ouiiiiiiiiiiiiiii

J’en profite pour faire remarquer qu’il est très classe de prononcer plusieurs fois très vite “sans paramètre, une propriété est plus appropriée”.

Enfin, il arrive qu’on ait besoin de spécifier des rôles techniques et des interactions entre plusieurs bouts de code : hiérarchie, composition, dépendances, etc. Ce sont les choses les plus compliquées à comprendre quand on lit du code : voir le tableau au complet, ce qui lie les différents blocs.

Il ne faut pas hésiter à nommer ses éléments pour cela. Apprendre le nom des design patterns aide beaucoup, mais même si on n’est pas top moumoute niveau vocabulaire, on peut faire des choses aussi simple que :

class BaseAuthenticator(object):
    #...
 
class PwdAuthenticator(BaseAuthenticator):
    #...
 
class KeyAuthenticator(BaseAuthenticator):
    #...

Si vous lisez BaseAuthentificator, vous n’avez pas besoin de voir qu’elle est parente d’autres classes plus bas pour savoir que ce n’est probablement pas une classe instanciable, mais sans doute une classe interface ou une classe abstraite. De quoi se faciliter une lecture en diagonale.

FAIL

Voici quelques exemples de noms qui ratent complètement l’objectif de documentation :

def do_query_database():
    # ...
 
def query_database():
    # ...
    do_query_database()
    # ...

J’en croise dans le code source de Django, et ça me fait hurler. Sérieux ça veut dire quoi ? Qu’est-ce qui a été extrait ? Dans quel but ? On a plus de question APRÈS avoir lu le nom qu’avant, c’est encore pire qu’un mauvais nom, c’est un nom méchant.

Dans ce cas, il faut essayer d’expliquer au maximum ce que l’appendice – qui va me faire choper une péritonite – que vous avez mis de côté fait :

def excute_and_send_query():
    # ...
 
def query_database():
    # ...
    excute_and_send_query()
    # ...

Un truc également exaspérant, c’est l’usage d’un vocabulaire ambigu :

def make_best_player_list():
    # ...

On sait ce que ça fait, ça fabrique une liste des meilleurs joueurs. Le contexte nous permet d’évaluer le résultat le plus probable. Maintenant un cas beaucoup moins clair :

def make_query():
    # ...

Ca envoie la requête, ça construit la requête ou les deux ? Make est un mot qui peut vouloir dire fabriquer ou exécuter. Ici il vaut mieux utiliser un vocabulaire plus explicite comme :

def build_sql_query():
    # ...

ou

def send_db_query():
    # ...

Là on sait qui fait quoi. Quitte à faire :

def db_query():
    build_sql_query()
    send_db_query()

Et oui, diviser le travail en plusieurs sous unités bien nommées, puis les regrouper dans un bloc plus général est aussi une forme de documentation. Créer des fonctions n’est pas qu’une question de maintenance ou de perf.

Savoir bien nommer les choses vient avec de l’entraînement. Au début, il faut prendre le temps de le faire. Il faut s’arrêter, et se mettre à la place d’un autre dev qui n’a pas encore eu son café.

Allez, détendez-vous, ce blog est plein de code qui ne suit pas les conseils de cet article. Faites juste au mieux ok ?

29 thoughts on “Bien nommer ses variables en Python

  • Christophe31

    Ils sont en vacance les relecteurs?

    “””
    Il n’ai pas rare que j’écrire des fonctions avec des noms bien dodu
    “””

  • Christophe31

    Ne validez pas mes commentaires, d’autres sites ont un flux exprès pour les remarques de ce genre sur les erreurs dans l’article.

    user, created = User.objects.get(username=”sam”)

    get au lieu de get_or_create

  • foxmask

    c’est l’heure de manger ; ca attendra ;) (j’en ai spotted une caisse ;)

  • Tiger-222

    Dans Habitudes stylistiques : Enfin, il arrive qu’on AIT besoin
    Dans Fail : ça fabrique une liste des meilleurs jour => joueurs non ?

    Bon article :) !

  • cendrieR

    Je me sens toujours sale quand je lis ce genre d’articles…

    (You are posting comments too quickly. Slow down.)

  • Christophe31

    get_or_create est bien une méthode du manager et pas une fonction, c’est get_object_or_404 qui est dans shortcut qui s’utilise comme ça ;-)

  • Alkareth

    Coquiiilles !
    “Nom, je parle de…” -> “Non” (au début de l’article).
    “top moumouth” -> là, j’ai en tête l’image d’un mammouth avec une perruque (blonde filasse), ce qui est particulièrement ridicule mais assez drôle. Plus classiquement, on a tendance à dire “top moumoute”, mais ça fait un effet de style, pourquoi pas.
    “un vocabulaire ambiguë” -> “ambigu”.

    Sinon excellent article, c’est le genre de règles que j’intuite sans jamais les définir réellement, pis des fois ça donne des trucs crades… Mais quand je vois ce que font certains de mes petits camarades de promo, je me sens comme un Victor Hugo de l’informatique entouré de Marc Lévy-s et autres Musso-s. C’est dur, des fois.

  • Sam Post author

    @Christophe31: je fais nawak moi aujourd’hui. Sur mon CV y marqué expert Django, bordel.

    @autres: corrigé !

  • Hipster

    Merci pour cet article, et pour tous les autres.

    Petite question, dans ce cas, la variable est vraiment inutile ? De mon point de vue, line.strip().strip() est exécuté à chaque tour de boucle, non ?

    > ici, la variable intermédiaire est inutile :
    >
    > for col in line.strip().split():
    > # do something

  • Sam Post author

    Non, l’expression de droite n’est évaluée qu’une fois pour toute la boucle. Heureusement, sinon ça aurait de gros effets de bord sur les générateurs.

  • JB

    Et du coup on pourrait avoir un article sur l’autre truc compliqué en programmation ?
    J’ai un peu testé des trucs mais sans trop savoir dans quoi me lancer: le mieux que j’ai fait (à la rache) est d’utiliser une méthode d’un model qui, une fois appelée, compile en HTML une “représentation” d’une instance, genre obj.detail, obj.as_td_in_table, obj.as_p, etc… et qui en plus stocke cette représentation dans la cache et stocke aussi le fait que cet objet est représenté dans tous ces endroits là… du coup en post_save: j’invalide toutes les clefs correspondantes à cette instance. J’ai même pas regardé ce qui existait par ailleurs. Bref ! Ca me semble assez léger quand même le simple cache par timeout de django.
    Un mot de votre part la dessus, j’en suis sûr, nous apporterait sinon la lumière, un certain éclairage.
    Merci encore pour vos articles (je me remets à peine de la découverte de WAMP).

  • yuiio

    > for col in line.strip().split():
    > # do something

    Ok j’ai bien compris line.strip().split() est executée une fois et évaluée avant le démarrage de la boucle : cette information vaut de l’or quand même ! … mais comment on fait pour savoir ça ? Je veux dire : quel moyen utilise t-on pour décortiquer tout seul et savoir ce que fait vraiment le code comme dans ce cas de figure : line.strip().spilt est executée à chaque tour de boucle ou bien une seule fois comme on vient de l’apprendre.

  • Sam Post author

    @JB: “un mot de votre part” ^^. Tu veux dire un dossier complet de 8 à 10 articles… Parce que le cache, c’est un sujet très, très large. Faut parler des formats, des systèmes de cache, du choix de ce que tu caches et à quel niveau, etc. Et je n’ai même pas encore mentionné l’invalidation du cache, là. On va finir le dossier test unittaires et angularjs avant. Et puis il y aussi le dossier regex qui n’a pas encore vu le jour.

    @yuiio: tu remplace l’expression par un appel de fonction qui retourne l’expression et qui fait un print, et tu regardes combien de fois le print s’affiche.

    @joshuafr: jutiliseTwisted = jutiliseArcMap = jutiliseDeVieuxModulesDeLaStdLIbGenreUnittest = FuckThem = jutilisePyQt4

  • toub

    Pour reprendre la question de yuiio:
    sur l’évaluation une seule fois par boucle du line.strip().split() , est-ce que ca dépend du contenu de la boucle ? Par exemple, si line est modifié dans la boucle, est-ce que python est suffisament malin pour réévaluer line.strip().split() ?

    Sinon super article encore une fois, merci! Moi j’ai l’habitude de préfixer mes variables également en fonction de leur type, pour faciliter la relecture. Python n’étant pas typé, c’est un peu bidon, rien n’empêche de fournir un objet d’un tout autre type que celui prévu initialement. Je viens du monde C/C++ ou c’est assez courant, on se refait pas! Ceci dit, j’avoue avoir un peu de mal quand je relis du code de lib python, où rien ne permet quand on lit une fonction, de savoir quels sont les types d’argument en entrée. Du coup j’apprécie quand même bien mon petit système de nommage

  • Sam Post author

    @toub : les chaines sont immutables, donc la ligne ne peut pas être modifiée dans la boucle. Si l’objet est mutable (par exemple une liste) et qu’il est modifié durant l’itération, Python lèvera une exception. Si on souhaite modifier un objet mutable par itération, soit on travaille sur une copie, soit on travaille par index. Pour le typage, la plupart des types des arguments sont généralement évidents, mais quand il ne le sont pas, ce doit être indiqués dans la doc string de la fonction. Si ce n’est pas fait, c’est du mauvais python.

  • MaNDRaXe

    Merci pour cet article très complet, mais si je peux me permettre, j’ai envie d’ajouter une autre recommandation destinée aux francophones : attention aux traductions en anglais.

    Il est très fréquent de constater des “coquilles” dans les nommages dues à des erreurs de traduction. J’en vois principalement 2 types :

    – googletranslate d’un mot français précis et utilisation aveugle du premier résultat retourné; la traduction automatique ne fonctionne pas pour les textes, mais le résultat est encore pire pour un terme isolé. C’est une technique très dangereuse car il y a de bonnes chances que le terme employé ne soit pas adapté. Une traduction inverse permet souvent de s’en rendre compte, mais une simple recherche sur le terme permet à coup sûr de valider le contexte habituel du terme trouvé.
    – les faux amis : c’est sûrement les pires à éviter car le dev n’a même pas conscience qu’il est en train de se tromper. Par exemple combien de français utilisent authentiFIcator au lieu de authenticator ? Et croyez moi, cela arrive même aux meilleurs ;-).

  • Sam Post author

    Authentificator, je l’ai sortie plusieurs fois moi-même.

  • MaNDRaXe

    @Sam : y compris dans cet article :-p.

    J’avoue que c’est ce qui m’a fait pensé à ma remarque.

  • NicoS

    Si seulement les développeurs du monde entier pouvaient lire cet article et mettre constamment en applications les principes que tu décris, ca m’éviterait bien des prises de tête au quotidien :( Quand je pense le temps que je peux passer à chercher le nom idéal pour cetaines variables, voir à les renommer deux/trois fois, et que je vois comment certains ne font aucun effort, des fois ca me décourage…
    Un petit bémol ceci dit, perso j’ai tendance à penser qu’au delà de trois mots, les noms de variables deviennent trop longs, et potentiellement que ca révèle une complexité trop importante et éventuellement du code mal fichu, mais certes, 4 voire 5 ca reste acceptable dans certaines circonstances.
    En tous cas ca me fait beaucoup penser à un article d’Atwood qui m’a beaucoup plu et mis le doigt exactement sur ce que je vivais à l’époque:
    http://blog.codinghorror.com/i-shall-call-it-somethingmanager/

  • Stephane

    Juste des coquilles :

    best = [k for k, v in data.items() if k > yop]
    yop -> top

    la convention et de rajouter -> est

    lambda x: sum(g for s, g x[1] if s == ‘A’) -> lambda x: sum(g for s, g in x[1] if s == ‘A’) #oubli du ‘in’

  • yom

    _ pour une variable qu’on ignore c’est vraiment une convention bien établie ?

    Parce que ça ne doit pas trop fonctionner avec la convention du module gettext qui utilise aussi _ comme alias …

  • Sam Post author

    C’est une convention bien établie. Ces deux codes ne se sont jamais rencontré dans ma vie. C’est assez logique. On va généralement utiliser _ pour une variable non utilisée pour des codes métiers, où la logique compte, et la sortie est destinée à une autre partie du programme. On utilisera par contre _ pour gettext dans des parties liées à l’UI (cli, log, gui, etc) ou aux données (accès base de données par exemple). Ce n’est pas du tout la même partie du code.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.