convention – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Bien nommer ses variables en Python http://sametmax.com/bien-nommer-ses-variables-en-python/ http://sametmax.com/bien-nommer-ses-variables-en-python/#comments Thu, 09 Oct 2014 09:42:31 +0000 http://sametmax.com/?p=12376 There are only two hard things in Computer Science: cache invalidation and naming things. Phil Karlton]]>

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 ?

]]>
http://sametmax.com/bien-nommer-ses-variables-en-python/feed/ 29 12376