Fonctions avancées : paramètres et annotations

Première leçon du cours Python intermédiaire (fonctions, modules, POO), en accès libre : environ 15 minutes, 3 exercices et 3 questions de quiz. C'est la leçon de base du catalogue ; dans l'espace étudiant, elle est réécrite pour ton profil.

Objectifs

  • Utiliser *args et **kwargs pour accepter un nombre variable d'arguments
  • Imposer des arguments nommés et éviter le piège des valeurs par défaut mutables
  • Annoter les types des paramètres et du retour

Paramètres avancésAnnotations de type

Fonctions avancées : paramètres et annotations

Vous savez écrire une fonction avec des paramètres et un return. Voyons les outils qui rendent les fonctions plus souples, plus sûres et plus lisibles.

Un nombre variable d’arguments : *args

def total(*montants):
    """Additionne autant de montants que l'on veut."""
    # montants est un tuple contenant tous les arguments positionnels
    return sum(montants)

print(total(10, 20))          # 30
print(total(5, 5, 5, 5))      # 20
valeurs = [1, 2, 3]
print(total(*valeurs))        # 6 : * déballe la liste en arguments

Le nom args est une convention ; c’est l’étoile qui compte.

Des options nommées : **kwargs

def creer_utilisateur(nom, **infos):
    # infos est un dictionnaire des arguments nommés supplémentaires
    fiche = {"nom": nom}
    fiche.update(infos)
    return fiche

print(creer_utilisateur("Lina", ville="Lille", age=31))
# {'nom': 'Lina', 'ville': 'Lille', 'age': 31}
options = {"ville": "Brest"}
print(creer_utilisateur("Yanis", **options))   # ** déballe un dictionnaire

L’ordre dans une signature est toujours : paramètres normaux, *args, paramètres nommés, **kwargs.

Forcer les arguments nommés

Un * seul dans la signature oblige à nommer les paramètres qui suivent. C’est précieux pour les options booléennes, illisibles en positionnel :

def exporter(donnees, *, separateur=";", entete=True):
    ...

exporter(lignes, separateur=",")   # clair
# exporter(lignes, ",", False)     # TypeError : arguments nommés obligatoires

Le piège des valeurs par défaut mutables

Une valeur par défaut est créée une seule fois, au moment où la fonction est définie. Avec une liste, elle est donc partagée entre les appels :

def ajouter(article, panier=[]):      # À ÉVITER
    panier.append(article)
    return panier

print(ajouter("pomme"))   # ['pomme']
print(ajouter("poire"))   # ['pomme', 'poire'] : surprise !

def ajouter(article, panier=None):    # version correcte
    if panier is None:
        panier = []
    panier.append(article)
    return panier

Les annotations de type

Les annotations indiquent les types attendus. Python ne les vérifie pas à l’exécution, mais votre éditeur et des outils comme mypy s’en servent pour détecter des erreurs, et elles documentent le code :

def moyenne(valeurs: list[float], arrondi: int = 2) -> float | None:
    """Renvoie la moyenne, ou None si la liste est vide."""
    if not valeurs:
        return None
    return round(sum(valeurs) / len(valeurs), arrondi)

Depuis Python 3.9, on écrit list[float] ou dict[str, int] directement ; depuis 3.10, float | None signifie « un float ou None ». Depuis Python 3.14, les annotations ne sont évaluées que lorsqu’on les consulte : une fonction peut être annotée avec une classe définie plus bas dans le fichier, sans guillemets (avant 3.14, cela provoquait une NameError).

À retenir

  • *args → tuple, **kwargs → dictionnaire ; * et ** déballent aussi à l’appel.
  • * seul rend les paramètres suivants obligatoirement nommés.
  • Utilisez None comme valeur par défaut pour les objets modifiables.
  • Annotez vos fonctions : c’est de la documentation vérifiable par les outils.

Exercices

Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.

  1. CodeDifficulté 2Python dans le navigateur2 indices

    Moyenne pondérée flexible

    Écrivez moyenne(*notes, arrondi=1) qui renvoie la moyenne des notes passées en arguments, arrondie à arrondi décimales. Si aucune note n’est donnée, renvoyez None. Annotez la fonction.

    def moyenne(*notes: float, arrondi: int = 1) -> float | None:
        ...
  2. CodeDifficulté 2Python dans le navigateur2 indices

    Corriger le défaut mutable

    La fonction ajouter_tag ci-dessous partage sa liste entre les appels. Corrigez-la pour que chaque appel sans tags parte d’une liste vide neuve, et rendez unique obligatoirement nommé (avec *). Si unique vaut True, le tag n’est pas ajouté s’il est déjà présent.

    def ajouter_tag(tag, tags=[], unique=False):
        if unique and tag in tags:
            return tags
        tags.append(tag)
        return tags
  3. QCMDifficulté 11 indice

    Contenu de kwargs

    Avec def f(a, *args, **kwargs): return kwargs, que renvoie f(1, 2, 3, x=4) ?

    • (2, 3)
    • {'x': 4}
    • {'a': 1, 'x': 4}
    • [2, 3, 4]

Quiz de fin de leçon

  1. Que se passe-t-il si l’on appelle f(1, 2) avec def f(a, *, b=0) ?

    • b vaut 2
    • TypeError
    • a vaut (1, 2)
  2. Les annotations de type sont-elles vérifiées par Python à l’exécution ?

    • Oui, toujours
    • Non, elles servent aux outils et à la documentation
    • Seulement pour les int
  3. Pourquoi écrire panier=None plutôt que panier=[] ?

    • C'est plus rapide
    • La liste par défaut serait partagée entre tous les appels
    • [] est interdit en défaut

La suite du cours

  1. Fonctions comme objets, lambda et générateurs20 min
  2. Modules, paquets et bibliothèque standard15 min
  3. Classes et objets20 min
  4. Héritage, dataclasses et exceptions personnalisées20 min

Python intermédiaire (fonctions, modules, POO) · Intermédiaire · 3 h