Cours de Python / Utiliser les arguments positionnels, nommés et les valeurs par défaut

Les arguments d’une fonction peuvent être transmis par position ou par nom ; une valeur par défaut rend un paramètre facultatif. Si chaque appel doit démarrer avec un nouvel état, évitez une liste modifiable comme valeur par défaut : cet objet serait partagé entre les appels qui omettent l’argument.

Rendre le sens des arguments visible

Les arguments positionnels sont associés aux paramètres dans l’ordre. Les arguments nommés, comme amount=9000, utilisent leur nom ; leur ordre relatif peut donc changer. Un même paramètre ne peut pas recevoir simultanément une valeur par position et une autre par nom. Un argument positionnel ordinaire ne peut pas non plus être placé après un argument nommé.

Dans def format_expense(name, amount, *, currency="KRW"):, les paramètres name et amount sont obligatoires. Le paramètre currency est facultatif grâce à son défaut et doit être nommé s’il est fourni, car il vient après le marqueur *. Ce marqueur ne reçoit pas lui-même de valeur. La règle rend l’unité visible dans l’appel.

Ces distinctions prolongent la leçon sur les paramètres, les arguments et les valeurs de retour. La documentation Python sur les valeurs par défaut et les arguments nommés détaille aussi les paramètres nommés uniquement.

Exécuter des appels avec options et listes distinctes

Python 3 et sa bibliothèque standard suffisent. Dans un dossier d’exercice, enregistrez le programme complet ci-dessous dans arguments.py, en UTF-8. Dans un terminal ouvert dans ce dossier avec l’environnement virtuel du projet activé, lancez python arguments.py. Sur macOS ou Ubuntu hors environnement virtuel, utilisez python3 arguments.py si votre interpréteur porte ce nom.

def format_expense(name, amount, *, currency="KRW"):
    return f"{name}: {amount:,} {currency}"

def append_name(name, names=None):
    if names is None:
        names = []
    names.append(name)
    return names

print(format_expense("lunch", 8000))
print(format_expense(amount=9000, name="dinner", currency="KRW"))
print(append_name("A"))
print(append_name("B"))

Le résultat attendu est :

lunch: 8,000 KRW
dinner: 9,000 KRW
['A']
['B']

Le premier appel utilise l’unité par défaut ; le deuxième nomme tous ses arguments et change l’ordre de name et amount. KRW désigne le won sud-coréen : le programme conserve cette unité et les données lunch et dinner de l’exercice. Le spécificateur :, ajoute ici des virgules entre groupes de milliers ; il ne choisit pas automatiquement un séparateur selon la langue du lecteur. Changer seulement le texte de currency ne convertit pas le montant dans une autre devise.

Les deux appels d’append_name qui omettent names obtiennent chacun une nouvelle liste. Le test names is None crée cette liste dans le corps au moment de l’appel, puis ajoute le nom. C’est pourquoi B ne se retrouve pas dans une liste contenant déjà A.

Observer quand la valeur par défaut est créée

Une expression de défaut est évaluée lorsque la définition de fonction est exécutée, pas à chaque appel. Avec names=[], une seule liste est créée pour cette définition et réutilisée par les appels qui n’en fournissent pas. Avec names=None, le défaut reste None mais le corps peut créer une nouvelle liste à chaque appel concerné.

Enregistrez le second programme complet dans defaults.py, dans le même dossier, et lancez python defaults.py — ou python3 defaults.py selon votre environnement. Les appels successifs doivent s’exécuter dans le même lancement pour observer le partage.

def shared(name, names=[]):
    names.append(name)
    return names
def fresh(name, names=None):
    if names is None:
        names = []
    names.append(name)
    return names
print(shared("A"))
print(shared("B"))
print(fresh("A"))
print(fresh("B"))

Le résultat attendu est :

['A']
['A', 'B']
['A']
['B']

La fonction shared conserve A dans son objet par défaut avant d’ajouter B. La fonction fresh construit une nouvelle liste pour chaque appel sans liste explicite. Relancer le script depuis le début exécute de nouveau les définitions ; il ne s’agit donc pas d’une conservation sur disque entre deux processus.

Le test is None conserve aussi une liste vide explicitement fournie. Le remplacer par if not names: traiterait cette liste vide comme une absence et lui substituerait un nouvel objet, contrairement à l’intention d’un appelant qui veut remplir sa propre liste.

Tester les appels incorrects et la modification d’un objet

  1. Ajoutez temporairement à la fin d’arguments.py un appel format_expense("lunch", 8000, "KRW"). Une TypeError signale que currency ne peut pas être fourni en troisième position. Utilisez currency="KRW" puis retirez cet essai.
  2. Dans le même fichier, après les appels existants, créez names = [], puis appelez append_name("A", names) et affichez names. Vous devez lire ['A'] : la fonction modifie intentionnellement la liste transmise. Retirez ces lignes et retrouvez les quatre lignes de référence.
  3. Dans defaults.py, ajoutez un troisième appel print(shared("C")) après les deux premiers appels de shared. La liste contient alors A, B et C. Retirez cet appel, relancez le fichier et retrouvez les quatre lignes initiales.

La modification d’une liste explicitement passée et le partage accidentel d’un objet par défaut sont deux questions distinctes. Le défaut None empêche ici le second phénomène, mais n’interdit pas le premier : documentez que la liste fournie sera modifiée. Si vous voulez préserver l’objet de l’appelant, choisissez explicitement une copie et définissez le niveau de copie nécessaire.

Une expression de date ou d’heure placée directement dans un défaut peut, elle aussi, conserver la valeur de la définition. Si une valeur fraîche est nécessaire à chaque appel, utilisez un marqueur d’absence puis calculez dans le corps. None convient seulement s’il n’est pas lui-même une valeur métier qu’il faut distinguer de l’argument omis ; sinon, un marqueur distinct est nécessaire.