Portée des variables (scope)
Rappel d'ouverture (5 minutes, cours fermé)
Réponds sans rouvrir les pages précédentes, en écrivant tes réponses.
- Quelle est la différence entre
return xetprint(x)dans une fonction ? - On écrit
b = carre(5). Explique en une phrase ce qui remplace l'appelcarre(5)dans cette ligne. - Où doit se trouver l'initialisation d'un accumulateur par rapport à la boucle, et pourquoi ?
Corrigé
returnrend une valeur au reste du programme, qui peut la ranger et la réutiliser.printne fait que l'afficher : la valeur est perdue pour le programme.- La valeur renvoyée remplace l'appel. La ligne devient
b = 25: un appel de fonction se comporte comme la valeur qu'il renvoie. - Avant la boucle. Placée dedans, elle serait remise à sa valeur de départ à chaque tour, et le résultat final ne compterait que le dernier élément.
La portée (scope) d'une variable détermine les parties du code où cette variable est accessible. Comprendre la portée permet d'éviter des bugs subtils et d'écrire du code plus clair.
1. Variables locales
Une variable locale est créée à l'intérieur d'une fonction. Elle n'existe que pendant l'exécution de cette fonction et n'est accessible que dans son corps.
Exemple de base
def ma_fonction() -> None:
x = 10 # Variable locale
print("Dans la fonction, x =", x)
ma_fonction() # Affiche 10
print(x) # ❌ ERREUR : x n'existe pas ici
Que se passe-t-il ?
- Quand
ma_fonction()est appelée, Python crée une variablexlocale - À la fin de l'exécution de la fonction,
xest détruite print(x)cherchexet ne la trouve pas → erreurNameError
Les paramètres sont aussi des variables locales
def calculer(a: int, b: int) -> int:
# a et b sont des variables locales
resultat = a + b # resultat est aussi locale
return resultat
somme = calculer(5, 3)
print(somme) # 8
print(a) # ❌ ERREUR : a n'existe que dans calculer()
Pourquoi les variables locales ?
Les variables locales permettent d'isoler le code de chaque fonction. Deux fonctions peuvent avoir des variables du même nom sans conflit :
2. Variables globales
Une variable globale est définie en dehors de toute fonction, au niveau principal du script. Elle est accessible partout dans le fichier.
Lire une variable globale
a = 5 # Variable globale
def afficher_a() -> None:
print("Dans la fonction, a =", a) # On peut lire a
afficher_a() # 5
print("En dehors, a =", a) # 5
Ici, pas de problème : on lit simplement la valeur de a.
Le piège : essayer de modifier sans global
x = 10 # Variable globale
def incrementer_x() -> None:
x = x + 1 # ❌ ERREUR : UnboundLocalError
print(x)
incrementer_x()
Que se passe-t-il ?
Python voit x = ... dans la fonction et décide que x est une variable locale pour toute la fonction. Donc quand il lit x + 1, il cherche une variable locale x qui n'a pas encore été créée → erreur.
Erreur conceptuelle courante
Beaucoup pensent que Python "lit d'abord la variable globale puis crée une locale", mais c'est faux. Python analyse la fonction avant de l'exécuter et décide que toute variable assignée (=) est locale. L'erreur se produit avant la lecture.
Modifier une variable globale avec global
Pour modifier une variable globale depuis une fonction, il faut déclarer explicitement global :
x = 10 # Variable globale
def incrementer_x() -> None:
global x # Déclare qu'on veut utiliser la variable globale
x = x + 1 # Maintenant ça marche
print("Avant :", x) # 10
incrementer_x()
print("Après :", x) # 11
Conflit de nommage : créer une variable locale masque la globale
compteur = 0 # Variable globale
def incrementer_compteur() -> None:
compteur = 5 # Crée une variable LOCALE "compteur"
print("Dans la fonction, compteur =", compteur)
incrementer_compteur() # Affiche 5
print("Global, compteur =", compteur) # Affiche 0 (inchangé)
Ici, la fonction crée une nouvelle variable locale compteur qui masque la globale. La variable globale reste à 0.
3. Bonnes pratiques
Éviter les variables globales modifiables
Les variables globales modifiables rendent le code difficile à comprendre et à débugger :
- N'importe quelle fonction peut changer leur valeur
- Difficile de savoir qui a modifié quoi
- Crée des dépendances cachées entre fonctions
Préférez :
- Passer les valeurs en paramètres
- Retourner les résultats avec return
Les constantes globales sont OK
Les variables globales en lecture seule (constantes) sont acceptables :
PI = 3.14159
TVA = 0.20
MAX_TENTATIVES = 3
def calculer_prix_ttc(prix_ht: float) -> float:
return prix_ht * (1 + TVA) # Lecture de TVA : OK
Par convention, on les écrit en MAJUSCULES.
Exception : Programmation événementielle
En programmation événementielle (interfaces graphiques, jeux vidéo), il est difficile d'éviter les variables globales modifiables. Les fonctions de callback (réactions aux événements) doivent accéder à l'état de l'application.
Exemple avec tkinter (interface graphique) :
import tkinter as tk
compteur = 0 # Variable globale nécessaire
def clic() -> None:
global compteur
compteur += 1
label.config(text=f"Clics : {compteur}")
fenetre = tk.Tk()
label = tk.Label(fenetre, text="Clics : 0")
label.pack()
bouton = tk.Button(fenetre, text="Cliquer", command=clic)
bouton.pack()
fenetre.mainloop()
Pourquoi c'est acceptable ici ?
- La fonction
clic()est appelée par le framework (pas par toi) - On ne peut pas passer de paramètres à
clic()directement - Les variables globales modélisent l'état de l'application
Dans ce contexte, c'est un compromis nécessaire pour les débutants. Les programmeurs avancés utilisent des classes (programmation orientée objet) pour mieux gérer l'état, mais en première, les variables globales sont acceptables pour la programmation événementielle.
4. Exercices
Exercice 1 : Prédire le résultat
Sans exécuter le code, prédis ce qui sera affiché :
y = 100
def tester() -> None:
y = 50
print("Dans tester, y =", y)
tester()
print("En dehors, y =", y)
Puis exécute pour vérifier.
Solution
Explication : La fonction crée une variable locale y = 50 qui masque la globale. La variable globale y reste à 100.
Exercice 2 : Identifier l'erreur
Ce code provoque une erreur. Pourquoi ? Comment la corriger ?
Solution
Erreur : UnboundLocalError: local variable 'total' referenced before assignment
Pourquoi : Python voit total = ... et décide que total est locale. Quand il lit total + valeur, il cherche une variable locale qui n'existe pas encore.
Correction 1 (avec global) :
total = 0
def ajouter(valeur: int) -> None:
global total
total = total + valeur
ajouter(5)
print(total) # 5
Correction 2 (meilleure : sans global) :
Exercice 3 : Gestion de stock
Crée un fichier stock.py avec :
- Une fonction
acheter(stock)qui diminue le stock de 1 et le retourne - Une fonction
livraison(stock, quantite)qui augmente le stock et le retourne
Teste avec un stock initial de 10, 2 achats, puis une livraison de 5.
Solution
def acheter(stock: int) -> int:
"""Diminue le stock de 1."""
return stock - 1
def livraison(stock: int, quantite: int) -> int:
"""Augmente le stock de la quantité livrée."""
return stock + quantite
# Test
stock = 10
print("Stock initial :", stock)
stock = acheter(stock)
stock = acheter(stock)
print("Après 2 achats :", stock) # 8
stock = livraison(stock, 5)
print("Après livraison de 5 :", stock) # 13
Exercice 4 : Portée dans les boucles (piège)
Que va afficher ce code ?
Puis exécute pour vérifier. Que se passe-t-il avec x et i ?
Solution
Attention : En Python, les variables créées dans une boucle for ne sont pas locales à la boucle, elles sont locales à la fonction. Donc x et i existent encore après la boucle.
C'est différent de langages comme C, Java ou Rust où les variables de boucle sont détruites après la boucle.
Exercice 5 : Accumulateur
Sans utiliser de variable globale, écris une fonction somme_chiffres(texte) qui renvoie la somme des chiffres présents dans une chaîne.
Teste avec "a1b2c3", qui doit donner 6.
Rappel : c.isdigit() dit si le caractère c est un chiffre, et int(c) le convertit en entier.
Solution
def somme_chiffres(texte: str) -> int:
"""Renvoie la somme des chiffres présents dans texte."""
total = 0 # variable LOCALE : elle disparaît à la fin de l'appel
for c in texte:
if c.isdigit():
total += int(c)
return total
# Test
print(somme_chiffres("a1b2c3")) # 6
L'accumulateur total est local : c'est ce qui permet d'appeler la fonction autant de fois qu'on veut sans que les appels se contaminent. Une variable globale, ici, serait un bug en attente.
5. Résumé
| Type | Où est-elle définie ? | Accessible où ? | Exemple |
|---|---|---|---|
| Locale | Dans une fonction | Uniquement dans cette fonction | def f(): x = 5 |
| Globale | Hors de toute fonction | Partout (lecture) | x = 5 (au niveau principal) |
| Globale modifiable | Hors de toute fonction | Partout (écriture avec global) |
global x; x = 10 |
Règles importantes :
- Les paramètres d'une fonction sont des variables locales
- Une assignation
x = ...dans une fonction crée une variable locale (sauf siglobal x) - Préférez paramètres + return aux variables globales modifiables
- Les variables de boucle
foren Python ne sont pas détruites après la boucle
Pour aller plus loin : Autres langages
D'autres langages ont des règles de portée plus strictes. Par exemple, en Rust, même les variables dans une boucle ont leur propre portée :
fn main() {
let x = 10;
for i in 0..3 {
let x = x + i; // Nouvelle variable locale à la boucle
println!("Boucle : x = {}", x);
}
println!("Après : x = {}", x); // x = 10 (inchangé)
}
Sortie :
En Rust, les variables globales modifiables nécessitent un bloc unsafe car elles sont considérées dangereuses. Python est plus permissif, mais cela ne signifie pas qu'il faut en abuser !
Vérification individuelle : les fonctions
À faire seul, cours fermé (15 minutes)
Sans aide et sans IA. Cette vérification conditionne le passage aux pages d'exercices en autonomie.
1. Lire. Que s'affiche-t-il ?
def double(n: int) -> int:
n = n * 2
return n
x = 5
y = double(x)
print(x, y)
print(double(double(2)))
2. Compléter. Cette fonction ne marche pas. Dis pourquoi avant de la corriger.
def moyenne_trois(a: float, b: float, c: float) -> float:
"""Renvoie la moyenne de trois nombres."""
print((a + b + c) / 3)
m = moyenne_trois(10, 12, 14)
print(m * 2)
3. Écrire. Depuis zéro : la signature typée, la docstring, deux assert, puis le code de nb_mots(phrase), qui renvoie le nombre de mots d'une phrase (les mots sont séparés par des espaces).
Réponses
1. 5 5 puis 8. Le paramètre n est local : le modifier dans la fonction ne touche pas x. Et un appel vaut la valeur qu'il renvoie, donc double(double(2)) vaut double(4), soit 8.
2. La fonction affiche au lieu de renvoyer. Elle rend donc None, et m * 2 lève une TypeError. C'est le piège return contre print. Il faut remplacer print(...) par return (a + b + c) / 3.
3.
def nb_mots(phrase: str) -> int:
"""Renvoie le nombre de mots de la phrase, séparés par des espaces."""
assert isinstance(phrase, str), "phrase doit être une chaîne"
mots = 0
en_mot = False
for c in phrase:
if c != " ":
if not en_mot:
mots = mots + 1
en_mot = True
else:
en_mot = False
return mots
assert nb_mots("bonjour le monde") == 3
assert nb_mots("") == 0
assert portent sur des cas différents, dont un cas limite.
Comment lire ton résultat
Les trois questions ne mesurent pas la même chose, et rater la troisième en réussissant la première n'est pas un signe de faiblesse. Lire du code, le compléter et l'écrire de zéro sont trois compétences distinctes, qui se travaillent séparément.
- Tu réussis les trois : passe à la suite.
- Tu réussis 1 et 2 mais pas 3 : tu comprends le mécanisme, il te manque la mise en route. Refais des exercices d'écriture courts, pas de la relecture.
- Tu rates la 1 : reprends le traçage avant tout le reste. Écrire du code qu'on ne sait pas lire ne mène nulle part.