Dev Python Senior

Comment écrire du code Python comme un développeur expérimenté ? Je te propose ici mes meilleures pratiques Python qui vont réellement distinguer un développeur sénior d’un développeur junior.
Au travers de divers exemples, nous explorerons les différences entre du code écrit par un développeur senior et celui écrit par un développeur junior.

En apprenant ces bonnes pratiques, tu pourras écrire un code qui sera non seulement perçu comme ayant été créé par un développeur expérimenté, mais qui sera également de meilleure qualité. Ces deux qualités seront un avantage lorsque tu présenteras ton code à tes collègues ou lors d’entretiens d’embauche.

1. Utiliser le bon type d’itérable

Un itérable est un objet Python capable de renvoyer ses membres un par un, ce qui lui permet d’être itéré dans une boucle for.

Source

Les développeurs débutants ont tendance à utiliser des listes chaque fois qu’ils ont besoin d’un itérable. Cependant, les différents types d’itérables ont des fonctions différentes en Python. Voici un résumé des itérables les plus essentiels :

  • Les listes sont destinées aux itérables qui doivent être ordonnés et mutables.
  • Les ensembles sont destinés aux itérables qui ne doivent contenir que des valeurs uniques et qui sont mutables et non ordonnés. Ils sont à privilégier pour vérifier la présence d’un élément, car ils sont alors extrêmement rapides. Toutefois, ils sont plus lents qu’une liste lorsqu’ils sont utilisés pour l’itération.
  • Les tuples sont destinés aux itérables qui doivent être ordonnés et immuables. Les tuples sont plus rapides et moins gourmands en mémoire que les listes.

Examinons d’abord la différence entre l’utilisation d’un ensemble (set) et celle d’une liste (list). Imaginons la tâche simple qui consiste à avertir l’utilisateur lorsqu’un nom d’utilisateur demandé est déjà utilisé. Par exemple, tu peux souvent rencontrer un code comme celui-ci :

requested_usernames = ["Tom123", "Sofia42"]
taken_usernames = []
for username in requested_usernames:
  if username not in taken_usernames:
    taken_usernames.append(username)
  else:
    print(f"Username '{username}' est déjà pris !")

took_usernames est une liste dans le code ci-dessus. Cependant, toutes les valeurs de taken_usernames ne doivent apparaître qu’une seule fois, il n’y a pas besoin de valeurs dupliquées, car les noms d’utilisateur dupliqués ne sont pas autorisés. De plus, le cas d’utilisation de taken_usernames est de vérifier la présence d’un nouveau nom d’utilisateur. Il n’y a donc aucune raison d’utiliser une liste. À la place, il est préférable d’utiliser un set, car nous avons lu plus haut que la vérification de la présence est plus rapide lorsqu’on utilise un set et parce qu’il n’y a pas besoin de stocker la même valeur plus d’une fois.

requested_usernams = ["Tom123", "Sofia42"] 
taken_usernames = set()
for username in requested_usernames:
  if username not in taken_usernames:
    taken_usernames.add(username)
  else:
    print(f"Username '{username}' est déjà pris !")

Bien que les deux extraits de code aboutissent au même résultat, l’utilisation d’un set pour vérifier la présence au lieu d’une liste montre aux autres que tu as compris qu’il existe différents types d’itérables pour différents cas d’utilisation, plutôt que d’utiliser une liste à chaque fois que tu as besoin d’un itérable.

Pour les itérables qui ne muteront pas pendant le temps d’exécution et qui ont besoin d’ordre, un tuple est la meilleure option ; par exemple :

# exemple plus débutant
weekdays = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]

for day in weekdays:
  ...

--------------------------------------------------------------------------
# exemple plus expérimenté
WEEKDAYS = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi")

for day in WEEKDAYS:
  ...

Tu sais maintenant mieux quand utiliser tel ou tel type d’itérable ! Ton code vous semblera déjà plus simple lorsqu’au lieu d’une liste, tu utiliseras un ensemble (set) pour les itérables qui ne contiennent que des valeurs uniques et qui n’ont pas besoin d’ordre, et un tuple pour un itérable ordonné dont les valeurs ne doivent pas changer.

Dans la prochaine section, nous examinerons les conventions de nommage de Python, après quoi nous comprendrons pourquoi, par exemple, WEEKDAYS a été orthographié avec des lettres majuscules dans l’exemple le plus expérimenté de l’extrait ci-dessus.

2. Utiliser les conventions de nommage de Python

Il existe deux types de « règles » pour les noms de variables en Python :

  • Règles imposées
  • Conventions d’appellation

Les règles imposées empêchent les noms de variables non valides, tels que les noms de variables commençant par un chiffre ou contenant des traits d’union :

2nd_name = "John"

# output 
SyntaxError: invalid syntax

---------------------------

second-name = "John"

# output 
SyntaxError: invalid syntax

Bien sûr, comme ces règles sont imposées par les interprètes Python, vous ne les verrez (heureusement) pas appliquées dans le code. Cependant, il existe des règles de style (aussi appelées conventions de nommage) pour les noms de variables qui ne sont pas appliquées, et donc, tu peux utiliser le mauvais style pour le mauvais objet.

Voici quelques-unes des conventions de nommage les plus importantes en Python :

variables : minuscules uniquement, avec des traits de soulignement pour séparer les mots, par exemple :

  • first_name items names_list

fonctions et méthodes : même règle que pour les variables, minuscules uniquement, avec des traits de soulignement pour séparer les mots, par exemple :

  • get_avg load_data print_each_item

classes : utiliser le CamelCasing ; commencer par une lettre majuscule et chaque nouveau mot commence par une autre lettre majuscule, sans soulignement entre les deux :

  • Person TrainStation MarineAnimal

constantes : majuscules uniquement, avec des traits de soulignement pour séparer les mots, par exemple :

  • WEEKDAYS FORBIDDEN_WORDS

modules : pour les noms de fichiers Python, utilise la même convention que pour les variables, les fonctions et les méthodes (minuscules et traits de soulignement pour séparer les mots) :

  • calculations.py data_preprocessing.py

L’utilisation des conventions de nommage appropriées n’est pas seulement un gage de maturité en Python, mais le fait de ne pas utiliser les conventions de nommage appropriées peut conduire à un code beaucoup plus confus, comme nous le verrons ci-dessous.

Code Python respectant les conventions de nommage PEP-8 :

# circle.py

PI = 3.14 # La valeur ne change pas, c'est donc une constante.

class Circle:
    def __init__(self, radius: float):
        self.radius = radius

    @property
    def area(self):
        return (self.radius **2) * PI
    
    @property
    def perimeter(self):
        return 2 * self.radius * PI

small_circle = Circle(1)
big_circle = Circle(5)

print(f"Aire du petit cercle = {small_circle.area}")
print(f"Périmètre du petit cercle = {small_circle.perimeter}")

print(f"Aire du petit cercle = {big_circle.area}")
print(f"Périmètre du petit cercle = {big_circle.perimeter}")

Code Python qui n’utilise pas les conventions d’appellation :

# CIRCLE.py

Pi = 3.14

class CIRCLE:
    def __init__(Self, RADIUS: float):
        Self.Radius = RADIUS

    @property
    def AREA(Self):
        return (Self.Radius **2) * Pi
    
    @property
    def PERIMETER(Self):
        return 2 * Self.Radius * Pi

SmallCIRCLE = CIRCLE(1)
BigCIRCLE = CIRCLE(5)

print(f"Aire du petit cercle = {SmallCIRCLE.AREA}")
print(f"Périmètre du petit cercle = {SmallCIRCLE.PERIMETER}")

print(f"Aire du petit cercle = {BigCIRCLE.AREA}")
print(f"Périmètre du petit cercle = {BigCIRCLE.PERIMETER}")

La plupart des développeurs douteront certainement de tes compétences en Python si tu leur fournis le code du deuxième extrait, alors que lorsque tu leur fournis le premier extrait, ils voient un code Python de qualité. Il est donc très important de veiller à respecter les conventions de dénomination de Python.

Pour plus de détails sur les conventions de nommage en Python, voir PEP-8.

3. Utiliser les déclarations de comparaison appropriées

Les opérateurs de comparaison (…) comparent les valeurs de part et d’autre et renvoient une valeur booléenne. Ils indiquent si une déclaration est vraie ou fausse en fonction de la condition.

Source

En Python, il existe de nombreuses façons d’écrire des instructions de comparaison (presque) identiques, mais elles ne sont pas nécessairement aussi appropriées les unes que les autres. Prenons un petit exemple :

def is_even(x):
  return True if x % 2 == 0 else False

x = 10

# différentes déclarations de comparaison qui aboutissent au même résultat :
if is_even(x) == True:
  print(f"{x} est un nombre pair !")

if is_even(x) is True:
  print(f"{x} est un nombre pair !")

if is_even(x):
  print(f"{x} est un nombre pair !")

Le dernier exemple if is_even(x) : fonctionne, car sans rien à comparer, Python considère if is_even(x) égal à True. Cependant, il est important de noter que presque toutes les variables Python seront évaluées à True, à l’exception de :

  • Les séquences vides, telles que les listes, les tuples, les chaînes de caractères (strings), etc.
  • Le nombre 0 (à la fois dans son type entier et dans son type flottant)
  • None
  • False (évidemment)

Cela signifie par exemple que chaque instruction if <number>: sera évaluée à True, sauf si ce nombre est 0. Par conséquent, une instruction if sans exemple concret peut sembler trop globale pour être utilisée, parce qu’il y a de fortes chances qu’elle soit évaluée à True sans qu’on le veuille. Pourtant, nous pouvons faire un très bon usage du fait que les séquences vides sont toujours évaluées à False, alors qu’une séquence avec au moins une valeur est toujours évaluée à True. Dans le code Python de base, tu rencontreras souvent l’instruction de comparaison suivante : if <variable> != [], comme dans l’extrait ci-dessous.

def print_each_item(items):
  if items != []:
    for item in items:
      print(item)
  else:
    raise ValueError("'items' est une liste vide, rien à imprimer !")

Cependant, que se passera-t-il si quelqu’un insère un autre type d’itérable ? Par exemple, un ensemble (set). Si tu veux lever une ValueError pour une liste vide, tu voudras probablement lever une ValueError pour un ensemble vide également. Dans le code ci-dessus, un ensemble vide sera toujours évalué à True, car un ensemble vide n’est pas égal à une liste vide. Une façon d’éviter ce comportement indésirable est d’utiliser if items au lieu de if items != [], car if items lèvera alors une ValueError pour chaque itérable qui est vide, y compris list, set et tuple de la section 1.

def print_each_item(items):
  if items:
    for item in items:
      print(item)
  else:
    raise ValueError("Aucun élément à imprimer à l'intérieur de 'items'")

Si tu souhaites comparer explicitement une valeur à True ou False, tu dois utiliser is True ou is False au lieu de == True et == False. Cela s’applique également à None, car ce sont tous des singletons (Voir PEP285). Bien que les différences de performances soient minimes, is True est un peu plus rapide que == True. Avant tout, cela montre que tu connais les PEP (Python Enhancement Proposals), ce qui témoigne de ta maturité en tant que développeur.

# exemple plus débutant
if is_even(number) == True:
  
# exemple plus expérimenté
if is_even is True:
  
-------------------

# exemple plus débutant
if value == None:
  
# exemple plus expérimenté
if value is None:

Astuce bonus : PEP8 met en garde contre l’utilisation de if value pour s’assurer que la valeur n’est pas None. Pour vérifier si une valeur n’est pas None, utilise explicitement if value is not None.

Le choix de l’instruction de comparaison la plus appropriée peut parfois t’éviter, à toi ou à d’autres, d’avoir à déboguer un bug délicat. Mais surtout, les développeurs chevronnés t’estimeront plus si tu utilises par exemple if value is True plutôt que if value == True.

Bien sûr, plutôt que d’écrire une instruction de comparaison pour la valeur d’une variable, il est préférable de vérifier d’abord le type de données, mais comment lever de bonnes exceptions pour cela ?
Voyons comment soulever des exceptions informatives dans cette section !

4. Lever des exceptions informatives

Une chose que les développeurs débutants font rarement est de lever « manuellement » des exceptions avec des messages personnalisés. Considérons la situation suivante : nous voulons créer une fonction appelée print_each_item() qui imprime chaque élément d’un type itérable.

La solution la plus simple serait la suivante :

def print_each_item(items):
  for item in items:
    print(item)

Bien entendu, ce code fonctionne. Toutefois, lorsque cette fonction fait partie d’une base de code importante, l’absence éventuelle de résultats d’impression lors de l’exécution du programme peut être déroutante. La fonction est-elle appelée correctement ? Une façon de résoudre ce problème est d’utiliser le logging, dont nous parlerons dans le point suivant. Tout d’abord, voyons comment prévenir les insécurités telles que l’absence de résultats d’impression en soulevant des Exceptions.

La fonction print_each_item() ne fonctionne que sur des objets Python itérables, de sorte que notre première vérification devrait être de savoir si Python peut itérer sur l’argument fourni en utilisant la fonction intégrée de Python iter() :

def print_each_item(items):

  # vérifier si les éléments sont itérables
  try:
    iter(items)
  except TypeError as error:
    raise (
      TypeError(f"items devrait être itérable, mais il est de type : {type(items)}")
      .with_traceback(error.__traceback__)  
    )

En essayant la fonction iter() sur items, nous vérifions s’il est possible d’itérer sur items. Bien sûr, il est également possible de vérifier le type des éléments avec isinstance(items, Iterable), mais certains types Python personnalisés peuvent ne pas compter comme Iterable alors qu’ils peuvent être itérables, donc iter(items) est plus étanche ici. Nous ajoutons ici le .with_traceback à l’Exception pour donner plus de contexte au débogage lorsque l’erreur est levée.

Ensuite, lorsque nous avons confirmé que items est itérable, nous devons nous assurer que items n’est pas un itérable vide, afin d’éviter l’absence de résultats d’impression. Nous pouvons le faire comme nous l’avons appris dans la section précédente, en utilisant if items:. Si if items: est False, nous voulons lever une ValueError, car cela signifie que l’itérable est vide. Voici ci-dessous la fonction print_each_item() qui a fait ses preuves :

def print_each_item(items):

  # vérifier si items est un Iterable
  try:
    iter(items)
  except TypeError as e:
    raise (
      TypeError(f"'items' devrait être itérable mais est de type : {type(items)}")
      .with_traceback(e.__traceback__)  
    )

  # si items est itérable, vérifier s'il contient des items
  else:
    if items:
      for item in items:
        print(item)

    # si items ne contient aucun élément, lever une ValueError
    else:
      raise ValueError("'items' ne doit pas être vide")

Bien sûr, le plus simple print_each_item() convient à la plupart des cas d’utilisation, mais si tu travailles comme développeur dans une entreprise, ou si tu écris du code open-source, et que la fonction est souvent réutilisée, tes collègues pythoniques pourraient exiger ou au moins être beaucoup plus heureux lorsqu’ils verront la fonction comme dans le deuxième exemple. Être capable de comprendre quelles exceptions peuvent se produire pour une fonction et comment les gérer correctement et soulever des exceptions informatives est certainement une compétence requise pour devenir (plus) senior.

Pourtant, ta fonction sera probablement refusée lorsqu’elle sera examinée par d’autres personnes. C’est parce qu’elle ne contient pas de docstring ou de type hinting (indication de type), qui sont essentiels pour un code Python de haute qualité.

5. Indication de type et docstrings

L’indication de type (type hinting) a été introduite dans Python 3.5. Avec l’indication de type, tu peux indiquer quel type est attendu. Un exemple très simpliste pourrait être :

def add_exclamation_mark(sentence: str) -> str:
  return f"{sentence}!"

En spécifiant str comme indication de type de sentence, nous savons que sentence doit être une chaîne de caractères et non, par exemple, une liste de mots. Avec -> str, nous spécifions que la fonction renvoie un objet de type string. Python n’impose pas les bons types (s’il ne lève pas une Exception si un objet d’un type différent est inséré), mais souvent les IDE comme Visual Studio Code et PyCharm t’aident à coder en utilisant les indications de type dans le code (voir la capture d’écran plus bas dans cette section).

Nous pouvons également l’appliquer dans notre print_each_item() :

from collections.abc import Iterable

def print_each_item(items: Iterable) -> None:
  ...

Les Docstrings permettent d’expliquer des extraits de code tels que des fonctions ou des classes. Nous pouvons ajouter la docstring suivante à print_each_item() pour que les autres utilisateurs et nous-mêmes sachions clairement ce que fait la fonction :

from collections.abc import Iterable

def print_each_item(items: Iterable) -> None:
  """
  Prints each item of an iterable.
  
  Parameters:
  -----------
  items : Iterable
      An iterable containing items to be printed.

  Raises:
  -------
  TypeError: If the input is not an iterable.
  ValueError: If the input iterable is empty.

  Returns:
  --------
  None
  
  Examples:
  ---------
  >>> print_each_item([1,2,3])
  1
  2
  3
  
  >>> print_each_item(items=[])
  ValueError: 'items' should not be empty
  """
  ...

Maintenant, si nous écrivons un code qui utilise print_each_item, nous voyons apparaître les informations suivantes :

Docstring meilleures pratiques Python

En ajoutant des indications de type et des chaînes de caractères, nous avons rendu notre fonction beaucoup plus conviviale !

Pour en savoir plus sur les indications de type, clique ici. Pour plus d’informations sur les docstrings, voir PEP-257.

Note : on peut avoir l’impression qu’écrire une longue docstring pour une fonction aussi simple est un peu exagéré, ce qui est parfois le cas. Heureusement, lorsqu’une fonction n’est pas secrète, tu peux facilement demander à ChatGPT d’écrire une docstring très précise et élaborée pour toi !

6. Utiliser logging

Il y a plusieurs choses qui rendent l’utilisation de ton code beaucoup plus agréable pour les autres, comme les indications de type et les docstrings. Cependant, l’une des fonctionnalités les plus importantes, sous-utilisées et sous-estimées est le logging. Bien que de nombreux développeurs (juniors) perçoivent le logging comme difficile ou inutile, l’exécution d’un programme correctement journalisé peut faire une énorme différence pour tous ceux qui utilisent ton code.

Seules deux lignes sont nécessaires pour rendre possible le logging dans ton code :

import logging

logger = logging.getLogger(__name__)

Désormais, tu peux facilement ajouter le logging pour faciliter, par exemple, le débogage :

import logging
from collections.abc import Iterable

logger = logging.getLogger(__name__)

def print_each_item(items: Iterable) -> None:
  """
  <docstring>
  """
  logger.debug(
    f"Printing each item of an object that contains {len(items)} items."
  )
  ...

Il peut également aider les autres développeurs à déboguer en enregistrant les messages d’erreur :

import logging
from collections.abc import Iterable

logger = logging.getLogger(__name__)

def print_each_item(items: Iterable) -> None:
  """
  <docstring>
  """
  
  logger.debug(
      f"Printing each item of an object that contains {len(items)} items."
    )
    
  # vérifier si items est itérable
  try:
    iter(items)
  except TypeError as e:
    error_msg = f"'items' should be iterable but is of type: {type(items)}"
    logger.error(error_msg)
    raise TypeError(error_msg).with_traceback(e.__traceback__)
  
  # si items est itérable, vérifier s'il contient des items
  else:
    if items:
      for item in items:
        print(item)
  
    # si items ne contient aucun élément, lever une ValueError
    else:
      error_msg = "'items' should not be empty"
      logger.error(error_msg)
      raise ValueError(error_msg)

Le logging étant une fonctionnalité rare dans le code des développeurs débutants, l’ajouter à ton propre code te rend déjà (apparemment) beaucoup plus expérimenté en Python !

Conclusion

Dans cet article, nous avons examiné 6 meilleures pratiques Python qui peuvent faire la différence entre un développeur junior et un développeur plus expérimenté. Bien sûr, il y a beaucoup plus de facteurs qui distinguent les développeurs seniors des juniors, mais en appliquant ces 6 meilleures pratiques, tu te distingueras définitivement (que ce soit à ton travail, lors d’un entretien de codage ou lorsque tu contribues à des paquets open-source) des autres développeurs juniors qui n’appliquent pas ces meilleures pratiques Python !

Pour apprendre à écrire du code de qualité en Python, je te recommande de suivre ce cours développeur Python.

Publications similaires

0 Commentaires
Le plus récent
Le plus ancien Le plus populaire
Commentaires en ligne
Afficher tous les commentaires