Apprenez en lisant dans l'ordre

Fonctions internes et clôtures — maîtriser la portée avec global et nonlocal

Apprends la portée des variables en Python avec les fonctions internes, les clôtures et les mots-clés global et nonlocal.

Dans l'article précédent, tu as vu comment se comportent les arguments et les valeurs de retour. Cette fois, tu vas creuser quelques sujets supplémentaires liés aux fonctions : comment Python sépare les variables à l'intérieur et à l'extérieur d'une fonction (portée), comment définir des fonctions à l'intérieur d'autres fonctions (fonctions internes), et comment retourner une fonction qui se souvient des valeurs externes (une clôture). Au passage, tu verras quand utiliser global et nonlocal pour réécrire une variable englobante depuis l'intérieur d'une fonction.

Variables globales et locales — des portées séparées

Les variables définies hors de toute fonction sont des variables globales ; celles définies à l'intérieur d'une fonction sont des variables locales. Tu peux lire une globale depuis l'intérieur d'une fonction, mais si tu affectes le même nom avec = valeur à l'intérieur de la fonction, Python crée une nouvelle variable locale — une chose distincte de la variable externe.

Si tu vérifies l'adresse mémoire d'une variable avec id(), tu verras que les versions externe et interne pointent vers des emplacements différents.

stock = 100   # variable globale

def show_stock():
    print(f"intérieur : {stock}")   # 100 — lit la valeur externe

def try_change():
    stock = 50                       # crée une toute nouvelle variable locale
    print(f"intérieur : {stock}")   # 50

show_stock()                         # intérieur : 100
try_change()                         # intérieur : 50
print(f"extérieur : {stock}")       # extérieur : 100  ← l'externe est inchangée
Intérieur et extérieur sont des portées séparées — vue ensembliste
Module (espace de noms global)
  • stock = 100 — variable globale
  • Les fonctions ne peuvent que la lire depuis l'intérieur
Espace de noms local de show_stock()
  • print(stock) → lit la valeur externe 100
  • Ne crée aucune variable locale
Espace de noms local de try_change()
  • stock = 50 — crée une nouvelle locale dans la fonction
  • N'affecte pas le stock externe
Les espaces de noms locaux s'imbriquent dans le module. Les lectures atteignent l'extérieur, mais les affectations restent enfermées comme des locales distinctes.

Utilise une variable globale stock pour confirmer que les lectures atteignent l'extérieur, mais les affectations à l'intérieur non.

① Déclare une globale stock = 100.

② Définis def show_stock(): et affiche f"intérieur : {stock}".

③ Définis def try_change(): et affecte stock = 50, puis affiche f"intérieur : {stock}".

④ Appelle show_stock()try_change()print(f"extérieur : {stock}") et vérifie que la valeur externe n'a pas changé.

(Lorsque la réponse est correcte, l'explication apparaîtra.)

Éditeur Python

Exécuter le code pour voir le résultat

Réécrire une variable externe avec le mot-clé global

Quand tu veux vraiment réécrire une globale externe depuis l'intérieur d'une fonction, déclare global nom_de_variable en tête de la fonction. Cela dit à Python : « c'est cette globale externe, pas une nouvelle locale. » Sans cela, une affectation comme count += 1 mélange une lecture et une écriture du même nom et tu obtiens UnboundLocalError (« local variable referenced before assignment »).

global déclare « cette variable externe »
sans globalcount += 1UnboundLocalErrorglobal countcount += 1réécrit lecount externeéchoueréussit
visit_count = 0

# × Sans global → UnboundLocalError
# def increment():
#     visit_count += 1
# increment()   ← UnboundLocalError

# ○ global te permet de réécrire la globale externe
def increment():
    global visit_count
    visit_count += 1

increment()
increment()
increment()
print(visit_count)   # 3

Garde global au minimum

global permet à une fonction de réécrire silencieusement l'état externe, ce qui rend, à grande échelle, une seule mauvaise écriture située 100 lignes plus loin étonnamment difficile à pister.

Par défaut, retourne une valeur avec return et affecte-la côté appelant, et tourne-toi vers les classes (vues plus tard) quand tu as vraiment besoin d'un comportement avec état.

Construis un compteur de visites de page en utilisant global.

① Déclare visit_count = 0 hors de toute fonction.

② Définis def increment_visit():. En tête de la fonction, écris global visit_count, puis visit_count += 1.

③ Appelle increment_visit() trois fois, puis affiche f"visites : {visit_count}".

Éditeur Python

Exécuter le code pour voir le résultat

Comment fonctions et variables vivent en mémoire — noms et choses

En interne, Python tient un espace de noms — une table de correspondance « nom → chose ». x = 5 dit : « crée l'entier 5 en mémoire et fais que le nom x pointe dessus. »

def f(): ... fonctionne pareillement : il construit un objet fonction (le corps de la fonction) en mémoire et fait que le nom f pointe dessus. Il y a exactement une de ces tables par programme — l'espace de noms global — créée au chargement du module et conservée jusqu'à la fin du programme.

Frames du module et des fonctions en mémoire
Espace de noms global (module)
  • x = 5 — le nom x pointe vers l'entier 5
  • def f — le nom f pointe vers un objet fonction
  • Reste vivant jusqu'à la fin du programme
Frame du premier appel à f()
  • Contient les arguments et les locales
  • Entièrement jeté au return
Frame du second appel à f()
  • Complètement séparé du premier
  • Les locales sont indépendantes
Chaque appel de fonction obtient un nouvel espace de noms local (frame) en mémoire, qui disparaît à la sortie. Il n'y a qu'un seul espace de noms global pour tout le programme.
Comment def et les appels de fonction utilisent la mémoire
①Exécute`def f(): ...`Enregistre la fonctiondans l'espace globalReste dans l'espace globaljusqu'à la fin②Premier appel : f()Crée un nouvelespace local (frame)Jette le frameau `return`③Second appel : f()Construit un nouveau frame(indépendant du #1)Jette le frameau `return`à l'exécutionappelà la sortierappelleà la sortie
Exécute def une fois et la fonction est enregistrée dans l'espace de noms global, où elle reste jusqu'à la fin. Chaque appel, en revanche, construit son propre frame local et le jette au retour. Les fonctions internes, clôtures et nonlocal qui suivent reposent tous sur cette structure de frames.

Fonctions internes — définir des fonctions dans des fonctions

Empile un autre def à l'intérieur d'une fonction et tu obtiens une fonction interne — une fonction qui n'existe que pour être utilisée à l'intérieur de la fonction externe. C'est un excellent moyen de nommer un bloc de logique significatif au sein d'une longue fonction, pour que le corps se lise comme une suite d'étapes claires.

Une fonction interne n'étant pas accessible depuis l'extérieur, elle convient aussi bien pour cacher des helpers que tu ne veux pas exposer.

Portée d'une fonction interne — vue ensembliste
Module (espace de noms global)
  • Appeler validate depuis ici → NameError
  • N'existe pas à l'extérieur
Frame de process_user()
  • Contient les arguments name / age
  • Peut appeler validate depuis l'intérieur
Frame de validate()
  • Lit les name / age externes
  • Pas exposé à l'extérieur
validate ne vit qu'à l'intérieur de process_user. Elle peut lire les arguments externes, et le monde extérieur ne la voit jamais.
def process_user(name, age):
    def validate():
        if not name or not isinstance(age, int) or age < 0:
            raise ValueError("Entrée invalide")

    validate()                       # appelle la fonction interne
    print(f"Traité : {name} ({age})")

process_user("Alice", 25)
# Traité : Alice (25)

# validate n'est pas accessible depuis l'extérieur de process_user
# validate()   ← NameError

Idéal pour découper une longue fonction

Dès qu'une fonction dépasse 50 ou 100 lignes, la découper en fonctions internes nommées d'après chaque bloc significatifprocess_name() / process_age() etc. — transforme la fonction externe en une liste lisible d'étapes. Si tu veux un jour en réutiliser une à l'extérieur, promouvoir une fonction interne en fonction normale est trivial.

Restructure une fonction de traitement de commande avec une fonction interne dédiée aux contrôles d'entrée.

① Définis def process_order(item, quantity):.

② À l'intérieur, définis def validate():. À l'intérieur, fais raise ValueError("Commande invalide") si item est vide, si quantity n'est pas un int, ou si quantity vaut 0 ou moins.

③ Appelle validate(), puis affiche f"Commande reçue : {quantity} x {item}".

④ Appelle process_order("pomme", 3) et vérifie le résultat.

Éditeur Python

Exécuter le code pour voir le résultat

Clôtures — des fonctions qui se souviennent de valeurs externes

Les fonctions internes peuvent lire les arguments et les locales de la fonction externe. Pousse l'idée d'un cran — fais que la fonction externe return la fonction interne — et tu as construit une fonction qui continue de fonctionner avec les valeurs externes intégrées. C'est une clôture.

C'est une manière propre de produire en série des fonctions similaires qui ne diffèrent que par un réglage, comme « une fonction qui triple » et « une fonction qui quintuple ». Prends le réglage (factor) en argument externe et référence-le depuis la fonction interne : make_multiplier(3) retourne « une fonction multiply qui se souvient de 3 », et make_multiplier(5) retourne « une fonction multiply qui se souvient de 5 ».

def make_multiplier(factor):
    def multiply(x):
        return x * factor    # référence le factor externe
    return multiply

times3 = make_multiplier(3)   # fonction qui se souvient de factor=3
times5 = make_multiplier(5)   # une fonction distincte qui se souvient de factor=5

print(times3(10))   # 30
print(times5(10))   # 50
print(times3(7))    # 21
Comment marche une clôture — définition et flux d'appel
①Appelmake_multiplier(3)②Frame externecréé avec factor=3④return multiply(se souvient de factor=3)③def multiplyréfère factor à l'intérieur⑤times3 = make_multiplier(3)⑥times3(10)→ 10 × 3(factor) = 30exécutedéfinit dedansretourne la fn qui se souvientreçoitappelle
Le frame externe de make_multiplier disparaît au retour, mais la fonction multiply construite dedans s'en va en se souvenant de factor=3.

Les clôtures distribuent des fonctions « préconfigurées »

Quand tu veux créer plein de calculs similaires qui ne diffèrent que par un réglage — « TVA 10 % » contre « TVA 8 % » etc. —, les clôtures brillent. Au lieu de passer le réglage à chaque appel, tu distribues une seule fonction préconfigurée, et le code appelant reste bien plus propre.

Construis une fonction qui retourne une fonction d'application de remise qui se souvient du taux de remise.

① Définis def make_discounter(rate):. À l'intérieur, définis def apply(price): qui retourne int(price * (1 - rate)). N'oublie pas return apply.

② Construis deux fonctions : discount_10 = make_discounter(0.1) et discount_30 = make_discounter(0.3).

③ Affiche discount_10(1000) et discount_30(1000), et vérifie que des taux de remise différents s'appliquent au même prix.

Éditeur Python

Exécuter le code pour voir le résultat

nonlocal — réécrire la variable d'une fonction englobante

Les clôtures peuvent lire la variable externe sans souci, mais comme avec global, essayer de la réécrire via count += 1 plante avec UnboundLocalError. Le mot-clé qui autorise cette réécriture est nonlocal. Là où global cible le niveau du module, nonlocal cible la locale de la fonction immédiatement englobante.

Encapsule l'état dans un objet fonction

nonlocal est la manière canonique d'encapsuler un compteur qui grandit à chaque appel dans une fonction.

Contrairement à global, l'état reste enfermé dans un objet fonction précis, donc les effets de bord ne se propagent pas, et tu obtiens un état plus sûr qu'en passant par une globale.

def create_counter():
    x = 0
    def increment():
        nonlocal x      # déclare qu'on met à jour le x de create_counter
        x += 1
        return x
    return increment

counter = create_counter()
print(counter())   # 1
print(counter())   # 2
print(counter())   # 3

# Un second compteur a son propre x indépendant
counter2 = create_counter()
print(counter2())  # 1 — sans rapport avec counter
nonlocal met à jour la variable de la fonction englobante — vue ensembliste
Module (espace de noms global)
  • counter = create_counter() — reçoit increment
  • Le code extérieur ne peut pas toucher x directement
Frame de create_counter()
  • x = 0 — compteur, créé une seule fois
  • Ce que increment référence via nonlocal
Frame de increment()
  • nonlocal x — pointe vers le x externe
  • x += 1 met à jour le x externe
nonlocal cible la variable de la fonction immédiatement englobante. L'état vit dans l'objet fonction, pas dans l'espace de noms global.

Construis quelque chose comme un générateur d'ID qui émet des IDs de commande à partir de 1, en utilisant une clôture avec nonlocal.

① Définis def create_order_id_issuer(): et initialise next_id = 1 en tête.

② À l'intérieur, définis def issue():. Après nonlocal next_id, écris current = next_idnext_id += 1return current.

③ Enfin, return issue pour rendre la fonction interne disponible.

④ Récupère-la via issue_id = create_order_id_issuer() et appelle print(issue_id()) trois fois — vérifie que tu vois 1 → 2 → 3.

Éditeur Python

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Qu'affiche ce code ?
stock = 100
def f():
stock = 50
f()
print(stock)

Question 2Pourquoi ce code lève-t-il une erreur ? Choisis la meilleure explication.
count = 0
def inc():
count += 1
inc()

Question 3Qu'affiche print(times3(10)) ?
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
times3 = make_multiplier(3)