ProPython
8 astuces pour écrire du code "Pythonic"
18 Apr, 2021

8 astuces pour écrire du code "Pythonic"

Python est un langage d'apparence simple à prendre en main, et avec lequel on peut développer des fonctionnalités complexes en relativement peu de lignes, et avec beaucoup de libertés. En revanche, cela mène souvent à écrire du code peu clair, difficile à débugger.

C'est pourquoi nous allons voir quelques astuces et quelques bonnes pratiques afin d'écrire du code plus efficace et mieux compréhensible, du code dit "Pythonic" par la communauté Python.

Assignement multiple et unpacking

Pour commencer, nous allons nous attaquer à une fonctionnalité de base et très pratique du Python : l'assignement multiple.

Cela consiste simplement à assigner une valeurs à plusieurs variables en une ligne, en chaînant les =.

>>> a = b = c = d = 10
>>> a
10
>>> d
10

Mais on peut également faire ça autrement si on souhaite affecter des valeurs différentes à nos variables :

>>> a,b,c,d = 1,2,3,4
>>> a
1
>>> c
3

L'unpacking est une fonctionnalité qui va de pair avec l'assignement multiple. Il nous permet de décomposer un itérable en plusieurs variables. Par exemple, si je possède une liste de prénoms, je vais pouvoir décomposer cette en liste et assigner pour chaque élément de la liste une variable.

>>> jean, bob, dylan = ["Jean", "Bob", "Dylan"]
>>> jean
'Jean'
>>> dylan
'Dylan'

Et cela marche avec tous les itérateurs !

>>> jean, bob, dylan = {0: "Jean", 1: "Bob", 2: "Dylan"}.values()
>>> jean
'Jean'
>>> bob
'Bob'

Arguments non nommés et nommés

Parfois, vous allez vouloir écrire des fonctions qui prennent un nombre indéfini de paramètres en entrée. Si vos paramètres n'ont pas de nom, vous pouvez éventuellement prendre en entrée une liste et effectuer des actions avec chaque élément de la liste. Si vos paramètres sont nommés, il vaut mieux utiliser un dictionnaire. Nous allons voir une autre façon de réaliser ceci.

Arguments non nommés

Imaginez que l'on souhaite écrire une fonction qui prend en entrée un nombre indéfini de prénoms et les affiche. Sans l'unpacking, il faut obliger à l'utilisateur de mettre ces prénoms dans une liste, et passer la liste en paramètre de la fonction. Voici ce que ça donne :

prenoms = ["Bob", "Dylan", "Jean"]
def my_func(prenoms):
    for prenom in prenoms:
        print(prenom)

my_func(prenoms)
"""Bob
Dylan
Jean"""

On aimerait avoir le même résultat en écrivant ceci, donc sans mettre nos prénoms dans une liste :

my_func("Bob", "Dylan", "Jean")

On pourrait donc écrire ça :

def my_func(prenom1, prenom2, prenom3):
    print(prenom1)
    print(prenom2)
    print(prenom3)

Mais on ne sait pas combien de prénoms on a en paramètres. C'est ici que l'unpacking et l'expression étoile * interviennent. L'étoile vous permet d'unpack un itérable. Voici un exemple :

values = (1, 2) # On initialise un tuple
print(values)
# (1, 2) => cela nous affiche notre tuple
print(*values)
# 1 2 => cela est équivalent à print(1, 2)
# ça décompose notre tuple, ça l'unpack

Dans le premier cas, cela nous affiche notre tuple, donc avec les parenthèses autour. Dans le second cas, l'affichage n'est pas du tout un tuple en tant que tel, mais les valeurs contenues dans notre tuple.

D'ailleurs remarquez que la fonction print est en fait exactement la fonction qu'on cherche à faire : elle prend un nombre indéfini d'arguments et les affiche à la suite.

Maintenant, mettons ça en application, on va prendre écrire une fonction qui prend en paramètres une variable avec l'expression étoile *.

def my_func(*prenoms):
    return

Cette fonction prend un nombre indéfini de paramètres. Ainsi, ce code ne génèrera pas d'erreurs :

my_func("Jean")
my_func("Jean", "Dylan")
my_func("Jean", "Dylan", "Bob", "blabla", "blablabla", "blablablabla")

Normalement vous devriez commencer à comprendre. Mettons ça en application maintenant pour afficher les paramètres reçus.

En parlant des paramètres reçus, on les récupère sous quelle forme dans notre fonction ?

Testez ! Et vous verrez... Ajoutez ceci dans votre fonction :

print(prenoms)

Cela vous affiche un tuple ! Et oui, ici c'est l'inverse de l'unpacking, c'est à dire qu'on a construit un itérable à partir de plusieurs éléments, et non l'inverse. C'est de cette manière que Python gère le nombre indéfini de paramètres, il met tout dans un tuple pour vous simplifier la vie. Comme ça vous pouvez simplement récupérer chaque paramètre en itérant sur votre tuple. Ou avec l'unpacking.

Revenons à notre fonction. Nous avons donc récupéré un tuple contenant les prénoms qu'on souhaite afficher. On pourrait donc itérer sur ce tuple et afficher les prénoms via une boucle for. Mais nous allons simplement unpack notre tuple dans la fonction print, puisque pour rappel celle ci prend un nombre indéfini de paramètres en entrée.

def my_func(*prenoms):
    print(*prenoms)
    return

Et voilà, nous avons créé une fonction qui affiche un nombre indéfini de paramètres (fonction inutile au passage car la fonction print réalise déjà ceci).

my_func("Jean")
# Jean
my_func("Jean", "Dylan", "Bob")
# Jean Dylan Bob

Arguments nommés

Bon, si vous avez compris la section précédente, vous allez comprendre celle-ci. Imaginons que vous souhaitez écrire une fonction qui prend un nombre indéfini de paramètres, mais dont le nom importe. Par exemple, on voudrait réaliser la fonction suivante :

afficher_scores(Bob=13, Dylan=4, Jean=23)
# Bob : 13
# Dylan : 4
# Jean : 23

Cette fonction affiche simplement le score d'un nombre indéfini de personnes passé en paramètres. On pourrait utiliser un dictionnaire pour réaliser ceci, mais ce ne serait pas tout à fait la même syntaxe :

personnes = {"Bob": 13, "Dylan":4, "Jean":23}
afficher_scores(personnes)

Nous ne voulons pas cette syntaxe.

On va donc utiliser l'expression double étoile ** qui permet le double unpacking, c'est à dire que cela permet d'affecter un mot-clé à une valeur.

def my_func(a, b, c):
    print(a, b, c)

my_func(**{"a": 1, "c": 3}, b=2)
# 1 2 3

En utilisant l'expression **, c'est comme si j'écrivais my_func(a=1, c=3, b=2). Ainsi, appliquons cela à notre exemple. Nous souhaitons récupérer un nombre indéfini de paramètres nommés. Par analogie avec l'expression étoile simple *, voici comment faire :

def afficher_scores(**personnes):
    return

On utilise cette fois ci deux étoiles. Si vous affichez personnes, vous remarquez que c'est un dictionnaire, dont les clés sont les noms des paramètres, et les valeurs leurs valeur. Python a donc pack les paramètres dans un dictionnaire. Maintenant, nous pouvons parcourir ce dictionnaire pour afficher nos scores :

def afficher_scores(**personnes):
    for prenom, score in personnes.items():
        print(prenom + " : " + str(score))

afficher_scores(Bob=13)
afficher_scores(Bob=13, Dylan=4, Jean=23)

Bien-sûr, ces fonctionnalités sont très puissantes et on peut faire énormément de choses avec les expressions simple et double étoile, mais ça vous vous en rendrez compte par vous-même en programmant.

Gestionnaires de contexte

Dans de nombreux autres langages, pour vérifier la bonne exécution de votre code lorsque vous manipulez des ressources sources d'erreurs (par exemple des fichiers, des flux de données), vous utilisez des blocs try/finally. Ceci est possible en Python également, mais il est mieux d'utiliser les gestionnaires de contexte, avec le mot-clé with. Un gestionnaire de contexte est un bloc de code vous garantissant une entrée, et vous garantissant une sortie. Voici un exemple, pour manipuler des fichiers :

# Mauvais
try:
    file = open("my_file.txt", "r")
    # ...
finally:
    file.close()
    
# Bien
with open("my_file.txt", "r") as file:
    # ...

Le code dans le with est assuré d'avoir un fichier à manipuler, et ce bloc nous permet d'être sûr que le fichier est bien fermé à la suite du bloc.

Nous pouvons développer nos propres gestionnaires de contexte. Pour cela il suffit de créer une classe et d'implémenter les méthodes spéciales __enter__ et __exit__ qui seront appelées respectivement en entrant et en sortant de ce contexte. La méthode __enter__ vous renvoie un objet que vous pouvez ensuite manipuler dans le contexte.

class ContextManager:

    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print("Entered with block")
        return "Hello " + self.name

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exited with block")

with ContextManager("Steph") as context:
    print(context)

"""Entered with block
Hello Steph
Exited with block"""

Notez qu'il existe une librairie très utile lorsque vous souhaitez utiliser des gestionnaires de contexte : contextlib. Plus d'infos ici : Documentation Contextlib.

Permutation de valeurs

En Python, on peut permuter les valeurs de deux variables simplement, sans nécessiter d'une troisième variable.

# Mauvais en Python
temp = a
a = b
b = temp

# Bien
a, b = b, a

Vous voyez, on peut faire ça en une ligne ! C'est fantastique Python quand même... Et en plus, ça marche aussi avec les itérables et leurs indices, par exemple les listes. Cela est très utile pour les tris.

a[i-1], a[i] = a[i], a[i-1]

La fonction zip

Je vous en ai sûrement déjà parlé. C'est une fonction utilisée pour manipuler plusieurs itérables en même temps. Cela est très utiles lorsque vous avez plusieurs listes à manipuler contenant des données liées entre elles.

personnes = [...]
scores = [...]
for personne, score in zip(personnes, scores):
    print(personne + " : " + score)

La fonction zip prend en entrée un nombre indéfini d'itérables et produit un générateur de tuples contenant les éléments de chaque itérable. Par exemple :

personnes = ["Bob", "Dylan", "Jean"] # Encore eux !
scores = [13, 4, 23]
# zip(personnes, scores) génèrera les tuples suivants :
# ("Bob", 12), ("Dylan", 4), ("Jean", 23)

Cette fonction est très efficace car elle fonctionne avec les générateurs, donc elle ne stocke charge pas les données en mémoire. Cependant cette fonction n'est pas vraiment pratique lorsque vous travaillez avec des listes de différentes longueurs. Dans ce cas, penchez vous du côté de la librairie itertools qui contient des outils relatifs aux itérateurs un peu plus spécifiques, comme itertools.zip_longest qui permet d'utiliser zip avec des listes de tailles différentes, en remplissant les valeurs manquantes avec None ou fillvalue si passée en paramètres.

Opérateur d'identité

L'opérateur d'identité est le mot-clé is. C'est à dire qu'il vérifie si deux objets sont identiques. Il faut cependant différencier identiques et égaux. Deux objets identiques sont exactement les mêmes, ils ne sont pas juste de valeur égale. Voici un exemple :

>>> a = 3
>>> b = 3
>>> a is b
True
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
False
>>> c = a
>>> a is c
True

Dans le premier cas, les deux entiers sont égaux, et identiques, ce qui est évident (on ne peut pas avoir plusieurs entiers de même valeur mais différents). Dans le deuxième cas, les listes contiennent les mêmes valeurs, mais ne sont pas les mêmes objets (ce qui n'est pas évident à saisir, mais imaginez deux personnes ayant le même prénom par exemple, ce n'est pas pour ça qu'elles sont identiques). Et dans le troisième cas, on crée une variable c identique à a, donc a is c est True.

Le plus souvent, nous nous intéressons aux valeurs, il faut donc utiliser ==, et n'utiliser is que lorsque vous vous intéressez à l'identité, ou dans les cas x is None, x is True, x is False, etc... Dans ces cas il est même mieux d'utiliser is puisque cela accélère légèrement votre programme.

Vérifier les arguments d'une fonction

Lorsque vous écrivez des fonctions qui prennent des arguments optionnels, il peut être utile de vérifier si ces arguments ont été effectivement fournis ou non, afin d'exécuter des actions différentes en fonction de cela. Une méthode simple est de mettre par défaut la valeur de ces arguments à None, et ensuite de vérifier s'ils ont été fournis de cette façon : if argument is None.

Mais comment faire si jamais None est une valeur possible pour votre argument ? Dans ce cas, si l'utilisateur écrit my_func(a, b=None), votre fonction va considérer que l'argument b n'est pas fourni alors qu'il l'est, car il vaut None. Pour cela, on peut initialiser une variable nulle (nulle ne signifie pas qu'elle vaut None car en soi None est une valeur) en utilisant object(). Voici comment faire :

_null = object() # On initialise une variable nulle

def my_func(x, y=_null):
    if y is _null:
        print("L'argument optionnel n'a pas été renseigné")
    else:
        print("L'argument optionnel a été renseigné")

my_func(1, 2) # L'argument optionnel a été renseigné
my_func(1, None) # L'argument optionnel a été renseigné
my_func(1) # L'argument optionnel n'a pas été renseigné

Le mot de la fin

Vous voyez, il existe plein d'astuces pour écrire du code clair et efficace en Python. En effet, Python étant un langage très peu restrictif, il est facile d'écrire du code difficile à débugger, ou peu clair. C'est pourquoi nous avons vu aujourd'hui 8 astuces basiques. Nous en verrons d'autres prochainement.

Si l'article vous a plus, que vous souhaitez faire un retour ou que vous avez des questions, n'hésitez pas à laisser un commentaire ou à me contacter via l'onglet disponible sur le site ou par mail.

A bientôt, pour la suite des astuces pour avoir un code plus Pythonic.

Laisser un commentaire

Premium - 15€/mois

L'accès à des articles inédits, à une multitude de ressources, à de nouveaux projets, mais également à des vidéos explicatives, découvrez ici pourquoi passer premium.

Articles liés

Catégories

Ressources

Retrouvez une collection de ressources (des scripts, des fiches résumé, des images...) liées aux articles du blog ou au Python.
Voir

Contact

contact@propython.fr
Se connecter pour envoyer un message directement depuis le site.

Navigation

AccueilSe connecterCréer un compteRessourcesPremium

Catégories

Pages légales

Politique de confidentialitéMentions légalesConditions générales de vente