5 pièges à éviter dans ton code (part. 1)

Retour à la liste des articles

17 mai 2020

Quand on commence à programmer (et même après !) il est normal de douter sans arrêt. Est-ce que mon code fonctionne ? Est-ce qu'il contient des bugs ? Est-ce qu'il est bien écrit ?

La réponse aux deux premières questions se trouve assez vite : il suffit d'exécuter son programme pour constater s'il fonctionne ou non. La troisième question, « est-ce que mon code est bien écrit ? » est plus difficile à évaluer sans l'avis d'une personne expérimentée. Cet article présente quelques principes qui t'aideront à améliorer la qualité de ton code, pour le rendre plus simple, plus compréhensible, et plus facile à débugger.

Voici sans plus attendre cinq conseils que j'aurais aimé qu'on me donne quand j'ai commencé à programmer.

1. Commentaires

Les trois erreurs fréquentes concernant les commentaires sont :

  1. Ne mettre aucun commentaire dans son code ;
  2. Mettre trop de commentaires ;
  3. Utiliser les commentaires pour désactiver des parties de son code.

Le but des commentaires dans le code est d'aider d'autres personnes à le comprendre. Même si tu travailles seul-e sur un projet tu devrais commenter ton code : parmi ces « autres personnes » il y a ton futur toi, qui se demandera pourquoi cette ligne de code a été mise là !

De manière générale, un commentaire devrait décrire pourquoi une ligne de code existe ou ses spécificités, plutôt que ce qu'elle fait. Si une ligne de code ne suffit pas à exprimer ce qu'elle fait, pose-toi la question suivante : « est-ce que je pourrais l'écrire différemment pour la rendre plus explicite, par exemple en créant une fonction ? » Seule exception à cette règle : les commentaires de fonctions, qui sont destinés aux personnes qui vont utiliser ton code et qui ont besoin de comprendre ce que fait une fonction, et comment l'utiliser.

Regarde le code suivant. Est-ce que tu penses que les commentaires aident à la bonne compréhension du code ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def language_change(request):
    # Retrieve the new language
    language = request.POST[LANGUAGE_QUERY_PARAMETER]

    # Check if the language is valid and that the user is authenticated
    if is_language_valid(language) and request.user.is_authenticated:
        # Set the user preferred language
        request.user.preferred_language = language
        # request.user.correspondance_language = language
        # Save the user
        request.user.save(update_fields=["preferred_language"])

    # Return a response with the language set
    return set_language(request)

Cette fonction a plusieurs problèmes :

  • Les commentaires ne font que répéter ce que le code exprime déjà clairement ;
  • La fonction n'a pas d'explications sur son utilité et son fonctionnement : pour l'utiliser, il faudra d'abord parcourir et comprendre son code ;
  • On ne sait pas quoi faire de cette ligne de code commentée (ligne 9). Pourquoi a-t-elle été laissée là ? Pourquoi a-t-elle été commentée ? Peut-on l'enlever ?

Compare maintenant cet extrait avec le code suivant, qui contient à mon avis juste la bonne quantité de commentaires. La fonction a un commentaire qui indique clairement ce qu'elle fait, ainsi que sa valeur de retour. Le commentaire sur l'avant-dernière ligne donne une indication supplémentaire à la personne qui lirait ce code et vient compléter l'instruction de la ligne 14.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def language_change(request):
    """
    Set the language cookie to the value of the post parameter
    `LANGUAGE_QUERY_PARAMETER`, and save it to the user preferences.
    Return the newly set language code.
    """
    language = request.POST[LANGUAGE_QUERY_PARAMETER]

    if is_language_valid(language) and request.user.is_authenticated:
        request.user.preferred_language = language
        request.user.save(update_fields=["preferred_language"])

    # Invalid languages are handled by set_language, no need for any additional check
    return set_language(request)

Lorsque tu apportes des modifications à ton code, n'oublie pas de relire les commentaires et de les adapter en conséquence.

2. Comparaisons avec des valeurs booléennes

Les valeurs booléennes sont parfois utilisées à tort dans des comparaisons, alors qu'elles ne devraient être utilisées que dans des assignations. Toute comparaison avec les valeurs True ou False devrait être supprimée de ton code.

Le code suivant contient trois comparaisons inutiles :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def ship(order):
    if order.is_draft() == True:
        raise ValueError("Cannot ship an order that is draft")

    if order.has_stock() == False:
        raise ValueError("Stock is empty for one or more items of the order")

    order.shipping_date = date.today()
    order.adjust_stock()
    order.save()

def has_stock(order):
    return all(item.stock > 0 for item in order.items.all()) == True

La comparaison order.is_draft() == True (ligne 2) est redondante, puisque order.is_draft() renvoie déjà True ou False. C'est pareil pour les comparaisons avec False (ligne 5), qui devraient être remplacées par not, par exemple if not order.has_stock(). La dernière ligne contient une autre comparaison avec une valeur booléenne, cette fois dans la valeur de retour.

Voici le même code, avec les comparaisons inutiles supprimées :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def ship(order):
    if order.is_draft():
        raise ValueError("Cannot ship an order that is draft")

    if not order.has_stock():
        raise ValueError("Stock is empty for one or more items of the order")

    order.shipping_date = date.today()
    order.adjust_stock()
    order.save()

def has_stock(order):
    return all(item.stock > 0 for item in order.items.all())

C'est plus lisible, non ?

3. Gestion des imprévus

Lorsque ton code atteint un cas que tu ne peux pas gérer, indique-le clairement en remontant une exception, plutôt que de retourner une valeur fantaisiste telle que None. Par exemple, la fonction suivante va chercher le profil d'une personne en faisant une requête HTTP :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def get_user_profile_data(email):
    response = requests.get(user_profile_url, params={"email": email})

    if not response:
        return None

    return response.json()


profile = get_user_profile_data("foo@bar.com")
if profile["receive_newsletter"]:
    send_newsletter("foo@bar.com")

Dans le cas où la requête échoue, la fonction renvoie la valeur None. Cela pose deux problèmes. Tout d'abord, le code qui appelle cette fonction n'a aucun moyen de savoir ce qui s'est mal passé. Est-ce que cette adresse mail n'existe pas ? Est-ce qu'il y a eu un problème lors du traitement de la requête ?

Ensuite, si le code qui appelle cette fonction ne gère pas la valeur None (ligne 11), alors le programme plantera avec une erreur cryptique telle que TypeError: 'Nonetype' object is not subscriptable. Les erreurs de ce genre peuvent être très difficiles à débugger.

Suivons mon conseil et utilisons des exceptions plutôt que la valeur None. Le code pourrait alors ressembler à ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ProfileDoesNotExist(Exception):
    pass

class ApiError(Exception):
    pass

class RequestError(Exception):
    pass


def get_user_profile_data(email):
    response = requests.get(user_profile_url, params={"email": email})

    if response.status_code == 404:
        raise ProfileDoesNotExist
    elif 500 <= response.status_code < 600:
        raise ApiError(response)
    elif not response:
        raise RequestError(response)

    return response.json()


profile = get_user_profile_data("foo@bar.com")
if profile["receive_newsletter"]:
    send_newsletter("foo@bar.com")

Si la requête échoue, le code qui appelle cette fonction peut clairement savoir pour quelle raison, et gérer chaque imprévu individuellement. De plus, si le code appelant oublie de gérer toutes les exceptions, alors le programme plantera avec un message d'erreur explicite (par exemple ProfileDoesNotExist).

4. Complexité du code

Plus le code prend de la place sur ton ordinateur, plus il en prendra aussi dans ta tête. On pourrait écrire des livres entiers sur la gestion de la complexité des programmes (d'ailleurs il en existe), mais je vais me contenter de deux conseils.

Le premier est de découper ton code en fonctions, qui font chacune une chose précise. Cela t'aidera à focaliser ton attention sur une seule chose à la fois, et réduira la quantité de code que tu dois garder dans ta tête. Chaque cas est unique, mais en ce qui me concerne j'essaie de limiter la taille de mes fonctions à un écran.

Le deuxième est de réduire le nombre d'embranchements que peut prendre ton programme. À chaque fois que tu ajoutes un niveau d'indentation dans ton code (par exemple en ajoutant un if, un for, ou une autre structure de contrôle), tu augmentes sa complexité. Le découpage en fonctions t'aidera à limiter le nombre d'embranchements visibles, et simplifiera donc la représentation mentale de ton code. Évite d'avoir plus de trois niveaux d'indentation.

À chaque fois que tu veux ajouter une fonctionnalité dans ton programme, je te recommande d'écrire une première version sans te préoccuper de la structure, puis, une fois qu'elle fonctionne, de lire ton code et d'en faire émerger des fonctions. Si tu peines à donner un nom à une fonction, c'est un bon indice que ton découpage n'est pas aussi logique que tu le croyais.

5. Répétitions

Les répétitions ont pour effet non seulement d'augmenter inutilement la quantité de code, et donc de complexifier sa lecture, mais elles augmentent aussi le risque de bugs : lorsque tu corriges quelque chose, tu devras penser à reporter la modification partout où ce bout de code a été copié.

À chaque fois que tu t'apprêtes à copier-coller ou recopier plus d'une ligne de code, pose-toi la question suivante : à quoi servent ces lignes ? Une fois que tu auras la réponse, crée une fonction qui porte ce nom (par exemple update_user_profile si les lignes servent à mettre à jour un profil), et mets les parties changeantes (s'il y en a) comme paramètres. Cela simplifiera ton code, et évitera que tu doives te rappeler de reporter les modifications à plusieurs endroits à chaque mois que tu changeras ce code.

Conclusion

  1. Enlève toutes les comparaisons avec des valeurs booléennes (== True et == False) ;
  2. Mets des commentaires pour expliquer les parties qui ne tombent pas sous le sens, ainsi que l'utilisation et les valeurs de retour des fonctions ;
  3. Crée et renvoie des exceptions lorsqu'une fonction ne peut pas traiter un cas, plutôt que de renvoyer la valeur None ;
  4. Découpe ton code en fonctions qui font une seule chose spécifique, et limite le nombre d'embranchements ;
  5. Plutôt que de répéter du code, déplace-le dans une fonction et appelle cette fonction autant de fois que nécessaire.
Portrait dessiné de Sylvain

Bonne programmation,
et à bientôt pour la deuxième partie de mes conseils géniaux,

Sylvain

Tu veux en savoir plus ?

Génies du code est une méthode illustrée, adaptée à tous les niveaux, qui t'initiera à la programmation à travers la réalisation de ton propre site web de A à Z. Les deux premiers chapitres sont disponibles gratuitement dans leur intégralité !

Découvrir Génies du code

Et aussi, fais un tour sur les autres articles, tous plus intéressants les uns que les autres, en toute modestie.