Retour au cours

backend / python

Fonctions : arguments, *args, **kwargs

Leçon 81 exercice

Explication

Ce que vous allez apprendre

  • Définir des fonctions avec des arguments positionnels, nommés et optionnels
  • Comprendre pourquoi une liste ou un dict comme valeur par défaut est un piège à éviter systématiquement
  • Utiliser *args et **kwargs pour accepter un nombre variable d'arguments
  • Imposer des arguments positional-only (/) ou keyword-only (*) pour concevoir une API stable
  • Déballer une liste ou un dictionnaire en arguments avec * et ** lors d'un appel

Dans quel contexte ?

Un développeur relit un bug étrange en production : une fonction ajouter_log(message, historique=[]) accumule silencieusement tous les messages jamais envoyés depuis le démarrage du serveur, même entre des appels qui n'ont rien à voir entre eux. Le coupable est la liste [] utilisée comme valeur par défaut, créée une seule fois à la définition de la fonction et partagée par tous les appels — un des pièges les plus tristement célèbres de Python, que cette leçon détaille pour de bon.

Emballer du comportement dans un nom

Une fonction, c'est un morceau de logique auquel on donne un nom pour pouvoir le réutiliser sans le réécrire. Au-delà de cette idée simple, Python offre une flexibilité rare dans la façon de définir les arguments qu'une fonction peut recevoir : positionnels, nommés, optionnels, ou en nombre variable.

Le piège le plus tristement célèbre du langage

Utiliser une liste ou un dictionnaire comme valeur par défaut d'un argument est une erreur extrêmement fréquente chez les débutants, car elle ne se voit pas immédiatement : la valeur par défaut n'est créée qu'une seule fois, au moment où Python lit la définition de la fonction, et non à chaque appel. Résultat, tous les appels qui n'indiquent pas cet argument partagent le même objet mutable, ce qui provoque des bugs difficiles à diagnostiquer. La solution universelle est d'utiliser None comme défaut et de créer l'objet réellement à l'intérieur du corps de la fonction.

Piège fréquent

def ajouter_log(message, historique=[]): partage la même liste historique entre tous les appels qui ne la précisent pas explicitement. Écrivez toujours def ajouter_log(message, historique=None): puis if historique is None: historique = [] à l'intérieur du corps.

*args et **kwargs : accepter l'imprévu

Ces deux syntaxes permettent à une fonction d'accepter un nombre variable d'arguments, respectivement positionnels et nommés. On les rencontre partout dès qu'on écrit des décorateurs ou des fonctions passe-plat qui relaient des arguments à une autre fonction sans connaître sa signature exacte à l'avance.

SyntaxeReçoitType dans le corps de la fonction
*argsArguments positionnels en surplustuple
**kwargsArguments nommés en surplusdict
*dimensions (à l'appel)Déballe une liste en positionnels
**params (à l'appel)Déballe un dict en nommés

Une discipline plus stricte avec / et *

Depuis Python 3.8, on peut imposer qu'un argument soit obligatoirement positionnel (avec /) ou obligatoirement nommé (avec *). C'est un outil de conception d'API : cela évite qu'un appelant s'appuie accidentellement sur un nom de paramètre qu'on voudrait pouvoir renommer plus tard sans casser le code appelant.

Astuce

Si vous écrivez une bibliothèque destinée à d'autres développeurs, marquez les paramètres internes en positional-only avec / : cela vous laisse la liberté de renommer ces paramètres plus tard sans casser le code de vos utilisateurs, puisqu'ils ne peuvent pas les appeler par leur nom.

Commandes & code

Fonctions : arguments, *args, **kwargs

Définir des fonctions flexibles et réutilisables.

python
# Fonction de base avec valeur de retour
def additionner(a, b):
    return a + b

# Arguments par defaut
def saluer(nom, message="Bonjour"):
    return f"{message}, {nom} !"

print(saluer("Alice"))
print(saluer("Bob", "Salut"))

# PIEGE classique : ne jamais utiliser un mutable comme valeur par defaut
def ajouter_mauvais(item, liste=[]):        # BUG : la liste est partagee entre appels !
    liste.append(item)
    return liste

print(ajouter_mauvais(1))    # [1]
print(ajouter_mauvais(2))    # [1, 2] -- inattendu !

def ajouter_correct(item, liste=None):
    if liste is None:
        liste = []
    liste.append(item)
    return liste

# Arguments positionnels et nommes
def creer_utilisateur(nom, age, ville="Paris"):
    return {"nom": nom, "age": age, "ville": ville}

creer_utilisateur("Alice", 30)                  # positionnel
creer_utilisateur(nom="Alice", age=30)          # nomme
creer_utilisateur("Alice", age=30, ville="Lyon")  # mixte

# *args : nombre variable d'arguments positionnels
def somme(*nombres):
    return sum(nombres)

print(somme(1, 2, 3, 4))    # nombres = (1, 2, 3, 4)

# **kwargs : nombre variable d'arguments nommes
def config(**options):
    for cle, valeur in options.items():
        print(f"{cle} = {valeur}")

config(debug=True, verbose=False, timeout=30)

# Combinaison complete et ordre impose
def fonction_complete(a, b, *args, c, d=10, **kwargs):
    print(a, b, args, c, d, kwargs)

fonction_complete(1, 2, 3, 4, c=5, e=6)
# a=1, b=2, args=(3,4), c=5, d=10 (defaut), kwargs={'e': 6}

# Parametres positional-only (/) et keyword-only (*) -- Python 3.8+
def diviser(a, b, /, *, precision=2):
    # a et b DOIVENT etre positionnels ; precision DOIT etre nomme
    return round(a / b, precision)

diviser(10, 3)                    # OK
diviser(10, 3, precision=4)       # OK
# diviser(a=10, b=3)              # TypeError : a et b sont positional-only

# Deballage d'arguments avec * et **
def volume(longueur, largeur, hauteur):
    return longueur * largeur * hauteur

dimensions = [2, 3, 4]
print(volume(*dimensions))        # deballe la liste en arguments positionnels

params = {"longueur": 2, "largeur": 3, "hauteur": 4}
print(volume(**params))           # deballe le dict en arguments nommes

# Type hints sur les fonctions (bonne pratique)
def calculer_moyenne(notes: list[float]) -> float:
    return sum(notes) / len(notes) if notes else 0.0

Résumé

  • Ne jamais utiliser une liste/dict mutable comme valeur par défaut : utiliser None puis initialiser dans le corps.
  • *args capture les positionnels restants, **kwargs les nommés restants.
  • / et * dans la signature imposent respectivement positional-only et keyword-only.

Exercices pratiques

1 disponible
1

Mission : traquer un historique de logs qui déborde entre requêtes

Objectif : Diagnostiquer et corriger le piège de l'argument par défaut mutable, puis concevoir une API de fonction robuste avec *args, **kwargs et keyword-only.

Contexte

Un serveur web expose def ajouter_log(message, historique=[]): historique.append(message); return historique. Chaque appel qui n'indique pas explicitement historique accumule silencieusement les messages de tous les autres appels précédents depuis le démarrage du serveur, y compris ceux d'autres utilisateurs. Personne ne comprend pourquoi les logs d'un utilisateur "fuient" chez un autre.

Tu dois expliquer et corriger ce bug, puis concevoir une fonction config(**options) capable de recevoir un nombre variable de paramètres nommés pour une future API de configuration.

Résoudre l’exercice →