Comment garder des lignes courtes ?


Arg, j’ai plein de commentaires sans réponse sur le blog, indexerror aussi. Des tickets millénaires sur 0bin et les impôts qui arrivent. Des fois ce qu’il faut c’est une petit victoire pour relativiser.

Du coup, un petit post pour me donner l’impression de ne pas être inutile à l’humanité.

De toutes les recommandations du PEP8, garder des lignes courtes, souvent moins de 80 caractères, est celle qui fait le plus râler.

On rétorque parfois que c’est une règle qui n’a pas de sens de nos jours avec nos grands écrans HD.

Sauf que:

  • L’œil humain fonctionne par micro sauts, et peut donc bien plus efficacement scanner de longues colonnes que de longues lignes.
  • Il est très courant de pouvoir mettre du code côte à côte sur un même écran pour comparer, merger ou avoir plusieurs vues sur un fichier.
  • Vous ne savez pas sur quoi sera lu votre code. Au bureau vous avez peut être un setup à triple écran 4K, mais les autres n’ont peut être pas cette chance. Les chats, les pastebins, les blogs posts, un slide show, un remote ssh, des comments, des tickets, ou un retro projecteur basse résolution sont autant de supports sur lesquels votre code finira probablement. Et ils ne sont pas larges.
  • Un diff sur de longues lignes c’est dégueulasse.
  • Le debug pas à pas qui s’arrête sur une ligne de 3km ça donne des envies de meurtre.

D’une manière générale, garder des lignes courtes est une bonne habitude et incitera à écrire un code sain. Le processus de réduire la taille des lignes passe par l’expressivité, la clarté, le refactoring, etc. Au final, c’est excellent pour la santé du projet.

N’oubliez pas non plus que le PEP8 indique clairement qu’il ne faut jamais suivre une règle de manière rigide. Si votre ligne est absolument plus adéquate sur une ligne, alors mettez la sur une ligne.

Maintenant qu’on a expliqué le pourquoi, parlons du comment. En effet beaucoup de bonnes pratiques ne sont pas appliquées parce qu’on ne sait pas faire. Faire des lignes courtes demande une certaine connaissance, et un peu de pratique.

Voici quelques pistes pour vous aider dans vos prochaines sessions.

Python est fait pour ça

Python force à indenter, et c’est une de ses meilleures caractéristiques. Jamais vous ne vous retrouverez comme dans d’autres langages face au code de votre collègue, ce gros porc, qui aligne ses accolades avec le groin.

Mais pour garder des lignes courtes, l’indentation joue contre vous.

Python vient heureusement avec tout un tas d’outils pour rendre votre code court, rapide et peu gourmand on mémoire. C’est tout bénef.

itertools, all(), any(), filter(), map(), collections, les listes en intension, functools et les générateurs sont tous d’excellents moyens de créer un code plus beau, plus court, et plus efficace. Apprenez à les utilisez ! Vous serez un meilleur programmeur, plus productif, et les lignes seront naturellement plus courtes.

Exemples…

Le module itertools permet de faire un tas de choses, comme remplacer les boucles imbriquées.

Remplaçons :

>>> for lettre in "abc":
...     for chiffre in (1, 2, 3):
...         print(lettre, chiffre)
...
a 1
a 2
a 3
b 1
b 2
b 3
c 1
c 2
c 3

Par :

>>> from itertools import product
>>> products = product("abc", (1, 2, 3))
>>> for lettre, chiffre in products:
...     print(lettre, chiffre)

Les built-ins ont un tas d’outils pour pour manipuler les données.

Si vous avez des collections dont vous voulez vérifier tout le contenu:

>>> from random import randint
>>> data = [randint(0, 10) for x in range(0, randint(0, 10))]
>>> max(data)  # pas besoin de boucler pour trouver le max
9
>>> if any(x > 3 for x in data): # any est un super "or"
...    print('Un élément est supérieur à 3')

Le module collections possède d’ailleurs tout un tas d’outils pour faire le travail à votre place :

>>> from collections import Counter
>>> Counter("aaaaaaaaabbbbbbccccc")
Counter({'a': 9, 'b': 6, 'c': 5})

On peut éviter un tas de try/except avec les helpers qui retournent une valeur par défaut:

>>> {"foo": 1}.get('bar', 3)  # et next, iter, setdefault... 
3

Sans parler de l’unpacking, le slicing, le paramétrage dynamique, etc.

Bref, si votre ligne est trop longue, c’est peut être parce que vous être en train de sous-utiliser Python.

De même, apprenez les libs les plus célèbres en Python.

pytest, pendulum, request, pandas…

Dès qu’un usage particulier intervient, elles seront généralement plus efficaces que la stdlib. Moins de code !

Refactorisez

Un bloc de code qui commence à être long sera avantageusement déplacé dans une méthode ou une fonction. Avec un joli nom bien explicite qui va naturellement rendre votre code mieux documenté, et testable.

Tant qu’on y est, faites usages des variables intermédiaires.

res = [foo for foo in objet.attribut['cle'] if foo > wololo]

Gagnera à devenir:

nom_explicite = objet.attribut['cle']
res = [foo for foo in nom_explicite if foo > wololo]

Les parenthèses pour les sauts de ligne

Les parenthèses en Python, c’est magique.

Avant :

from module_de_la_mort_qui_tue import foo, bar, et, toute, la, clique

Après

from module_de_la_mort_qui_tue import (
	foo, 
	bar, 
	et, 
	toute, 
	la, 
	clique
)

Ca marche partout, dans les appels de fonctions, l’accès des attributs, les chaînes de caractères…

On part de :

queryset = ModelDeDjango.objects.filter(attribut=val).first()

Et bim :

queryset = (ModelDeDjango.objects
                         .filter(attribut=val)
                         .first())

Si des parenthèses sont déjà là, inutile d’en rajouter:

Aller :

raise OverkillError("Arrrrrrrrrrrrrrrrrrrrrrrrrrrrggggggg")

Hop :

raise OverkillError(
	"Arrrrrrrrrrrrrrrrr"
	"rrrrrrrrrrrggggggg"
)

Aidez-vous de l’environnement

Vous outils sont là pour vous faciliter la vie. Votre éditeur par exemple, a certainement un moyen d’afficher un indicateur vertical pour symboliser la limite de 80 caractères.

Les bons IDES et les linters vous signalerons le dépassement. Ça vaut le coup d’investir dedans. Si votre éditeur ne le supporte pas par défaut, il a peut-être un plugin pour utiliser un linter.

VSCode et Sublime Text ont tous deux des plugins pour Python qui permettent cela. Notamment ils peuvent exploiter l’excellent linter flake8, qui sinon peut s’utiliser à la main.

Pour certaines lignes, vous allez dépasser. Ce n’est pas la mer à boire tant que c’est un choix conscient et pas de la flemme.

Par exemple, les commentaires avec des URLs dedans dépassent souvent. Beaucoup de linters ont un réglage pour les autoriser (ou un pattern au choix), activez-le.

Si vous avez une ligne qui dépasse et que le signalement de votre IDE/linter vous casse les couilles, il est souvent possible de le désactiver en ajoutant en fin de ligne le commentaire:

# noqa

Qui signifie tantôt “no question asked” ou “no quality assessment”.

16 thoughts on “Comment garder des lignes courtes ?

  • entwanne

    Bel article.

    Je suis quand même dubitatif sur les commentaires du type #noqa pour désactiver temporairement des règles de lint. Je trouve que ça en laidit le code et n’a pas grand chose à y faire (ça ne remplit pas la fonction de commentaire).

    Au passage, quelques typos relevées :

    Dans le 3ème bloc de code : le commentaire décrit all mais c’est any qui est utilisé ;
    « aventagement » → « avantageusement » ;
    « & » → « est ».

  • Sam Post author

    Au contraire, ce commentaire est génial. Quand tu lis le code, il indique clairement que le précédent dev pensais “je sais que c’est pas idéal, mais c’est le moins pire que j’ai trouvé”.

    Merci pour les corrections.

  • Orwell

    Quelques petites coquilles :

    * l’impression de servir ne pas être inutile à l’humanité.

    * Ce n’est pas la mère à boire

    Sinon j’ai bien aimé le premier exemple qui donne une ligne plus grande que celles avec les boucles imbriquées ;)

  • Matt

    Pareil, je trouve ces commentaires utiles, ça montre que celui qui est passé par là c’est ce qu’il a fait (bon, sous réserve qu’il mette pas systématiquement un commentaire sans se poser de question dès que le linter gueule…).

    Par contre je suis pas fans des commentaires génériques désactivant complètement le linter, je préfère désactiver spécifiquement la règle qui a été violée. Comme ça si plus tard la ligne est modifiée et se met à violer une autre règle, ça sera signalé, et il faudra corriger ou ajouter la règle au commentaire.

  • blackmoor

    Typo:

    Dois fois ce qu’il faut c’est une petit victoire pour relativiser.

    Merci pour l’article.

  • Alphonse la Défonce

    Sam je t’aime. Excellentes justifications à l’emploi de <80 caractères – perso j’ai souvent 3 à 5 colonnes juxtaposées sur un même écran, parfois même pour visualiser en même temps 2 portions d’un même fichier. L’argument des micro sauts oculaires paraît capillotracté, mais t’as bien fait de le citer en 1er parce que c’est putain de vrai.

    Coquilles :

    1) “Dois fois” au tout début => je pense que tu voulais écrire “des fois”, allez, écris-donc “parfois” et on n’en parle plus.

    2) “Jamais vous ne vous retrouverez comme dans d’autres langages face au code de votre collègue, ce gros porc, aligne ses accolades avec le groin.” => il manque des mots genre “face au code [adjectif_bien_senti] de votre collègue, qui, ce gros porc, …”

    3) anventageusement au lieu d’avantageusement mais ça a déjà été remarqué je crois

  • Cym13

    Maintenant que j’y pense c’est sans doute aussi plus performant d’écrire sur 80 colonnes. Je pense aux outils unix qui travaillent essentiellement sur des lignes:

    grep donnera des résultats plus ciblés donc moins d’écritures différentes
    git aura moins à stocker dans ses diffs
    les outils de rendu type terminal n’ont pas de retour à la ligne à calculer

    Ok, c’est peanuts, mais bizarrement il y a toujours quelqu’un pour prendre l’argument de l’optimisation inutile ;)

  • Louis

    Perso j’essaye d’utiliser le minimum de variable possible.

    Je ne créé une variable que si elle peut factoriser au moins trois fois une expression.

    Du coup ça fait des lignes un peu longue. Par exemple une méthode qui affiche la liste des attributs d’un objet de manière allignée et indentée correctement.

    def str(self):

    ….return ‘\n’.join([‘\t’.join([str(val) for val in [clef.ljust(max([len(key) for key in self.dict])), valeur]]) for clef, valeur in self.dict.items()])

  • Cym13

    @Louis: Euh… Pourquoi au juste ? Quel est l’intérêt de ne pas nommer ces valeurs intermédiaires que l’on doit de toute manière se farcir pour comprendre la fonction ? Donner un bon nom aux choses est une aide précieuse, notamment lorsqu’il s’agit de déterminer si un comportement est attendu ou non dans du code que l’on a pas écrit.

    • Louis

      @Cym13: Pour ne pas encombrer la mémoire et pour faire tenir mes fonctions sur moins de 5 lignes. (En fait c’est parce que c’est pénible de trouver un nom au variable, donc si c’est pour appeler mes variables intermédiaires a, b ou c ça vaut pas la peine ;-)

    • Louis

      @Brice : Aucun c’est pour un projet perso. Je fais des lignes comme ça plus par défi que par conviction.

  • Heretron

    Je pense que la limite de 79 caractères est un des trucs que je préfère en python.

    C’est comme lire du texte sur un livre de poche vs du A4 imprimé en paysage. Les magazines / journaux l’ont bien compris, le texte est sur de petites colonnes en général. En C++/Java/Whatever les gens ont la sale habitude de faire des lignes de 3km, ça demande une charge mentale énorme à la compréhension, on passe son temps à se focaliser sur des détails et ça demande des heures pour comprendre l’image générale.

    @Louis: C’est justement parce que c’est pénible qu’il faut le faire ! :)

    Tu t’es frappé la compréhension pour l’écriture du code, tu peux épargner ce travail à tes relecteurs.

    L’idée c’est aussi d’avoir une seule action par ligne.

    Je suis en train de décortiquer ton code depuis 5min pour arriver à me l’approprier.

    str(self):

    return '\n'.join(['\t'.join([str(val) for val in [clef.ljust(max([len(key) for key in self.dict])), valeur]]) for clef, valeur in self.dict.items()])

    De base j’ai un ascenseur horizontal, rien ne t’empêche de coller des sauts de ligne dans cette expression, tu es au milieu de parenthèses / crochets.

    En faisant des lignes courtes je remarque que ton affaire pourrait être plus simple :

    – tu as une boucle for inutile pour coller un tab entre la clée paddée et la valeur

    – tu converti la clé en str via ljust et via str()

    – tu calcules plein de fois la longueur max d’une clé, en passant en variable c’est plus explicite et tu fais le calcul une seule fois.

    def str(self):

    maxkeylen = max([len(key) for key in self.dict])

    return '\n'.join([

    clef.ljust(maxkeylen) + '\t' + str(valeur)

    for clef, valeur in self.dict.items()

    ])

    Ton exemple se prête un peu trop bien au jeu pour illustrer, si tu as des cas plus tordus fais péter ! :)

    PS: Sam, je crois bien que c’est 79 et pas 80 caractères

    https://www.python.org/dev/peps/pep-0008/#maximum-line-length

  • Brice

    @Louis : Et y’a combien de relecteurs qui se sont suicidés à cause de toi?

    @entwanne : il faut limiter ce commentaire aux cas où c’est vraiment un commentaire. On ne l’écrit pas (uniquement) pour désactiver un linter, mais pour informer le lecteur de la volonté express d’écrire une ligne de code qui en général n’est pas souhaitable. Genre :

    from shared_settings import * # noqa

    Ce noqa signifie : Je sais qu’en général il est vilain de faire un import *, mais en l’occurrence, c’est exactement ce que je veux faire, c’est pas une erreur.

Comments are closed.

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