21. Portée des variables et résolution des noms
Lorsque vous créez une variable en Python, où « vit-elle » ? Une fonction peut-elle voir des variables créées en dehors d’elle ? Du code en dehors d’une fonction peut-il accéder à des variables créées à l’intérieur ? Ces questions concernent la portée (scope) — la région de votre programme où un nom est visible et peut être utilisé.
Comprendre la portée est crucial pour écrire des fonctions qui fonctionnent correctement et de manière prévisible. Sans ces connaissances, vous pourriez accidentellement créer des bugs où les variables n’ont pas les valeurs attendues, ou où les modifications des variables ne persistent pas comme prévu.
Dans ce chapitre, nous allons explorer comment Python détermine à quelle variable un nom fait référence, comment contrôler où les variables sont accessibles, et ce qui se passe lorsque vous supprimez un nom. À la fin, vous comprendrez les règles qui régissent la visibilité des variables dans les programmes Python.
21.1) Variables locales et globales
Chaque variable en Python existe au sein d’une portée spécifique — une région de code où ce nom de variable est défini et accessible. Les deux portées les plus fondamentales sont locale et globale.
Comprendre la portée globale
Les variables créées au niveau supérieur de votre programme — en dehors de toute fonction — existent dans la portée globale. On les appelle des variables globales, et elles sont accessibles depuis n’importe où dans votre module après avoir été définies.
# Variable globale - définie au niveau du module
total_users = 0
def show_user_count():
# Cette fonction peut LIRE la variable globale
print(f"Total users: {total_users}")
show_user_count() # Output: Total users: 0
print(total_users) # Output: 0Dans cet exemple, total_users est une variable globale. À la fois la fonction show_user_count() et le code au niveau du module peuvent y accéder. Considérez les variables globales comme visibles dans l’ensemble de votre fichier de programme.
Comprendre la portée locale
Les variables créées à l’intérieur d’une fonction existent dans la portée locale de cette fonction. On les appelle des variables locales, et elles ne sont accessibles qu’au sein de la fonction où elles sont définies. Une fois l’exécution de la fonction terminée, les variables locales disparaissent.
def calculate_discount(price):
# discount_rate est LOCALE à cette fonction
discount_rate = 0.15
discount_amount = price * discount_rate
return discount_amount
result = calculate_discount(100)
print(result) # Output: 15.0
# Ceci provoquerait une erreur - discount_rate n'existe pas ici
# print(discount_rate) # NameError: name 'discount_rate' is not definedLes variables discount_rate et discount_amount n’existent que pendant l’exécution de calculate_discount(). Après le retour de la fonction, ces noms n’existent plus. C’est en réalité une bonne chose — cela évite aux fonctions d’encombrer votre programme avec des variables temporaires.
Pourquoi la portée locale est importante
La portée locale fournit une encapsulation — chaque fonction dispose de son propre espace de travail privé. Cela signifie que vous pouvez utiliser les mêmes noms de variables dans différentes fonctions sans conflit :
def calculate_tax(amount):
rate = 0.08 # Variable locale
return amount * rate
def calculate_shipping(weight):
rate = 5.00 # Variable locale différente portant le même nom
return weight * rate
tax = calculate_tax(100)
shipping = calculate_shipping(3)
print(f"Tax: ${tax}") # Output: Tax: $8.0
print(f"Shipping: ${shipping}") # Output: Shipping: $15.0Les deux fonctions utilisent une variable nommée rate, mais ce sont des variables complètement distinctes dans des portées locales différentes. Les changements de rate dans une fonction n’affectent pas rate dans l’autre fonction. Cet isolement rend les fonctions plus fiables et plus faciles à comprendre.
Lire des variables globales depuis des fonctions
Les fonctions peuvent lire des variables globales sans aucune syntaxe particulière :
# Configuration globale
max_login_attempts = 3
def check_login(password):
# Lecture d'une variable globale
if password == "secret123":
return "Login successful"
else:
return f"Invalid password. You have {max_login_attempts} attempts."
result = check_login("wrong")
print(result) # Output: Invalid password. You have 3 attempts.La fonction check_login() peut lire max_login_attempts parce que c’est une variable globale. Cependant, il y a une limitation importante à comprendre.
La règle : l’affectation crée des variables locales
C’est ici que la portée devient délicate. Si vous affectez une valeur à un nom de variable à l’intérieur d’une fonction, Python crée une nouvelle variable locale portant ce nom, même si une variable globale portant le même nom existe :
counter = 0 # Variable globale
def increment_counter():
# AVERTISSEMENT : ceci crée une NOUVELLE variable locale nommée counter - uniquement pour la démonstration
# PROBLÈME : tentative de lecture de counter avant de lui affecter une valeur localement
counter = counter + 1 # UnboundLocalError: local variable 'counter' referenced before assignment
print(counter)
# increment_counter() # This call results in UnboundLocalErrorCe code échoue parce que Python voit l’affectation counter = counter + 1 et décide que counter doit être une variable locale. Mais ensuite, lorsqu’il essaie d’évaluer counter + 1, la variable locale counter n’a pas encore de valeur — nous essayons de l’utiliser avant de lui avoir affecté une valeur.
C’est une source fréquente de confusion. La règle est : si une fonction affecte une valeur à un nom de variable n’importe où dans son corps, ce nom est traité comme local dans toute la fonction, même avant l’affectation.
Voyons cela plus clairement :
message = "Hello" # Variable globale
def show_message():
print(message) # Ceci fonctionne - on lit simplement la globale
def change_message():
# AVERTISSEMENT : ceci illustre une erreur courante - uniquement pour la démonstration
# PROBLÈME : Python voit l'affectation plus bas, donc message est traité comme local partout
print(message) # UnboundLocalError!
message = "Goodbye" # Ceci rend message local pour TOUTE la fonction
show_message() # Output: Hello
# change_message() # This call results in UnboundLocalErrorLa fonction show_message() fonctionne très bien parce qu’elle ne fait que lire message. Mais change_message() échoue parce que l’affectation sur la deuxième ligne pousse Python à traiter message comme local dans toute la fonction, y compris dans l’instruction print() qui précède l’affectation.
Les paramètres sont des variables locales
Les paramètres d’une fonction sont des variables locales qui reçoivent leurs valeurs initiales à partir des arguments passés lors de l’appel de la fonction :
def greet(name): # 'name' est une variable locale
greeting = f"Hello, {name}!" # 'greeting' est également locale
return greeting
message = greet("Alice")
print(message) # Output: Hello, Alice!
# Ni 'name' ni 'greeting' n'existent ici
# print(name) # NameErrorLe paramètre name n’existe qu’à l’intérieur de la fonction greet(). Il est créé lorsque la fonction est appelée et disparaît lorsque la fonction se termine.
Exemple pratique : calcul d’un panier d’achat
Voyons comment les portées locale et globale fonctionnent ensemble dans un scénario réaliste :
# Configuration globale
tax_rate = 0.08
free_shipping_threshold = 50
def calculate_total(subtotal):
# Variables locales pour ce calcul
tax = subtotal * tax_rate # Lecture de la variable globale tax_rate
# Déterminer le coût d'expédition
if subtotal >= free_shipping_threshold: # Lecture du seuil global
shipping = 0
else:
shipping = 5.99
total = subtotal + tax + shipping
return total
# Calculer pour différentes valeurs de panier
cart1 = calculate_total(30)
cart2 = calculate_total(60)
print(f"Cart 1 total: ${cart1:.2f}") # Output: Cart 1 total: $38.39
print(f"Cart 2 total: ${cart2:.2f}") # Output: Cart 2 total: $64.80Dans cet exemple :
tax_rateetfree_shipping_thresholdsont des valeurs globales de configurationsubtotal,tax,shippingettotalsont locales à chaque appel decalculate_total()- Chaque appel de fonction obtient son propre ensemble de variables locales distinctes
- La fonction peut lire la configuration globale mais ne la modifie pas
Cette séparation des responsabilités rend le code clair : les variables globales contiennent une configuration qui s’applique partout, tandis que les variables locales contiennent des résultats de calcul temporaires spécifiques à chaque appel de fonction.
21.2) La règle LEGB pour la résolution des noms
Quand Python rencontre un nom de variable, comment sait-il de quelle variable vous parlez ? Python suit un ordre de recherche spécifique appelé la règle LEGB. LEGB signifie Local, Enclosing, Global, Built-in — les quatre portées que Python examine, dans cet ordre.
Les quatre portées dans LEGB
Comprenons chaque portée dans la hiérarchie LEGB :
- Local (L) : la portée de la fonction actuelle
- Enclosing (E) : la portée de toute fonction englobante (les fonctions qui contiennent la fonction actuelle)
- Global (G) : la portée au niveau du module
- Built-in (B) : les noms intégrés de Python, comme
print,len,int, etc.
Quand vous utilisez un nom de variable, Python cherche dans ces portées dans l’ordre : L → E → G → B. Il utilise la première correspondance qu’il trouve et arrête la recherche.
Portée locale : le premier endroit où Python regarde
Python vérifie toujours d’abord la portée locale :
def calculate_price():
price = 100 # Variable locale
tax = 0.08 # Variable locale
total = price * (1 + tax)
return total
result = calculate_price()
print(result) # Output: 108.0Quand Python voit price, tax et total à l’intérieur de calculate_price(), il les trouve dans la portée locale et utilise ces valeurs. La recherche s’arrête à la portée locale — Python n’a pas besoin de chercher plus loin.
Portée globale : quand la portée locale ne l’a pas
Si un nom n’est pas trouvé localement, Python vérifie la portée globale :
# Variables globales
default_tax_rate = 0.08
default_currency = "USD"
def calculate_price(amount):
# 'amount' est local, trouvé immédiatement
# 'default_tax_rate' n'est pas local, trouvé dans la portée globale
total = amount * (1 + default_tax_rate)
return total
result = calculate_price(100)
print(result) # Output: 108.0Quand Python rencontre default_tax_rate à l’intérieur de la fonction, il ne le trouve pas localement, donc il cherche dans la portée globale et l’y trouve.
Portée intégrée : les noms prédéfinis de Python
Si un nom n’est pas trouvé dans la portée locale ou globale, Python vérifie la portée intégrée — les noms que Python fournit automatiquement :
def process_data(numbers):
# 'numbers' est local
# 'len' n'est ni local ni global - il est built-in
count = len(numbers)
# 'max' est aussi built-in
maximum = max(numbers)
return count, maximum
data = [10, 25, 15, 30, 20]
result = process_data(data)
print(result) # Output: (5, 30)Les noms len et max ne sont pas définis dans votre code — ce sont des fonctions intégrées que Python fournit. Quand Python ne trouve pas ces noms localement ou globalement, il vérifie la portée intégrée et les y trouve.
Portée englobante : fonctions imbriquées
La portée englobante entre en jeu lorsque vous avez des fonctions imbriquées — des fonctions définies à l’intérieur d’autres fonctions. C’est ici que le « E » de LEGB devient important :
def outer_function():
outer_var = "I'm from outer" # Dans la portée englobante pour inner_function
def inner_function():
inner_var = "I'm from inner" # Local à inner_function
# inner_function peut voir à la fois inner_var (local) et outer_var (englobant)
print(inner_var) # Output: I'm from inner
print(outer_var) # Output: I'm from outer
inner_function()
outer_function()Pour inner_function(), la portée de outer_function() est une portée englobante. Lorsque inner_function() référence outer_var, Python cherche :
- Dans la portée locale de
inner_function()— non trouvé - Dans la portée englobante de
outer_function()— trouvé ! Utilise cette valeur
LEGB en action : exemple simple
Voyons les quatre portées travailler ensemble dans un exemple clair et direct :
# Built-in : len (Python fournit ceci)
# Global : multiplier
multiplier = 10
def outer(x):
# Portée englobante pour inner
y = 5
def inner(z):
# Portée locale
# z est local (L)
# y vient de la portée englobante (E)
# multiplier vient de la portée globale (G)
# len vient de la portée built-in (B)
result = len([z, y, multiplier]) # Utilise les quatre portées !
return z + y + multiplier
return inner(3)
answer = outer(100)
print(answer) # Output: 18Quand Python évalue z + y + multiplier à l’intérieur de inner() :
- L (Local) : trouve
z = 3 - E (Enclosing) : trouve
y = 5dansouter() - G (Global) : trouve
multiplier = 10 - B (Built-in) : trouve la fonction
len
Cet exemple démontre clairement comment Python recherche dans les quatre portées pour résoudre les noms.
Masquage : quand les portées internes cachent les noms externes
Si le même nom existe dans plusieurs portées, la portée la plus interne « gagne » — c’est ce qu’on appelle le masquage :
value = "global"
def outer():
value = "enclosing"
def inner():
value = "local"
print(value) # Which value?
inner()
print(value) # Which value?
outer()
print(value) # Which value?Output:
local
enclosing
globalChaque instruction print() voit une valeur différente de value parce que Python s’arrête à la première correspondance :
- À l’intérieur de
inner(): trouvevaluelocalement → affiche "local" - À l’intérieur de
outer()mais en dehors deinner(): trouvevaluedans la portée deouter()→ affiche "enclosing" - Au niveau du module : trouve
valueglobalement → affiche "global"
Visualiser l’ordre de recherche LEGB
Ce diagramme montre le processus de recherche de Python. Il commence à la portée la plus interne et progresse vers l’extérieur. Si le nom n’est trouvé dans aucune portée, Python lève une NameError.
Pourquoi LEGB compte pour écrire des fonctions
Comprendre LEGB vous aide à :
- Prédire les valeurs des variables : vous savez exactement quelle variable Python va utiliser
- Éviter les conflits de noms : vous comprenez quand des noms se masquent mutuellement
- Concevoir de meilleures fonctions : vous pouvez décider quelle portée est appropriée pour chaque variable
- Déboguer les problèmes de portée : quand les variables n’ont pas les valeurs attendues, vous pouvez remonter LEGB
La règle LEGB est fondamentale dans la façon dont Python résout les noms. Chaque fois que vous utilisez une variable, Python suit cette règle en arrière-plan.
21.3) Utiliser le mot-clé global avec précaution
Nous avons vu que les fonctions peuvent lire des variables globales, mais que faire si vous devez modifier une variable globale depuis l’intérieur d’une fonction ? C’est là que le mot-clé global intervient — mais il doit être utilisé avec parcimonie et précaution.
Le problème : l’affectation crée des variables locales
Comme nous l’avons appris plus tôt, affecter une valeur à une variable dans une fonction crée une variable locale :
counter = 0 # Variable globale
def increment():
# AVERTISSEMENT : ceci crée une NOUVELLE variable locale nommée counter - uniquement pour la démonstration
# PROBLÈME : tentative de lecture de counter avant de lui affecter une valeur localement
counter = counter + 1 # UnboundLocalError!
# increment() # This call results in UnboundLocalErrorCela échoue parce que Python voit l’affectation et traite counter comme local dans toute la fonction. Mais nous essayons de lire counter avant de lui avoir affecté une valeur localement.
C’est l’une des erreurs les plus courantes lorsqu’on travaille avec des variables globales. Le message d’erreur UnboundLocalError: local variable 'counter' referenced before assignment vous dit exactement ce qui s’est passé : Python a décidé que counter était local (à cause de l’affectation), mais vous avez essayé de l’utiliser avant de lui donner une valeur.
La solution : déclarer les variables comme globales
Le mot-clé global dit à Python : « Ne créez pas une nouvelle variable locale avec ce nom. Utilisez la variable globale à la place. »
counter = 0 # Variable globale
def increment():
global counter # Dire à Python d'utiliser le counter global
counter = counter + 1 # Désormais, ceci modifie la variable globale
print(f"Before: {counter}") # Output: Before: 0
increment()
print(f"After: {counter}") # Output: After: 1
increment()
print(f"After again: {counter}") # Output: After again: 2La déclaration global counter doit venir avant d’utiliser la variable. Elle indique à Python que toute affectation à counter dans cette fonction doit modifier la variable globale, et non créer une variable locale.
Plusieurs variables globales
Vous pouvez déclarer plusieurs variables comme globales en une seule instruction :
total_sales = 0
total_customers = 0
def record_sale(amount):
global total_sales, total_customers
total_sales += amount
total_customers += 1
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $0, Customers: 0
record_sale(25.50)
record_sale(30.00)
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $55.5, Customers: 2À la fois total_sales et total_customers sont déclarées globales, donc la fonction peut modifier les deux.
Quand utiliser global : état partagé
Le mot-clé global est approprié quand vous devez maintenir un état partagé — des données auxquelles plusieurs fonctions doivent accéder et qu’elles doivent modifier :
# État du jeu
player_score = 0
player_lives = 3
game_over = False
def award_points(points):
global player_score
player_score += points
print(f"Score: {player_score}")
def lose_life():
global player_lives, game_over
player_lives -= 1
print(f"Lives remaining: {player_lives}")
if player_lives <= 0:
game_over = True
print("Game Over!")
def check_game_status():
# On lit seulement des globales - pas besoin du mot-clé global
if game_over:
return "Game Over"
else:
return f"Playing - Score: {player_score}, Lives: {player_lives}"
# Jouer au jeu
award_points(100) # Output: Score: 100
award_points(50) # Output: Score: 150
lose_life() # Output: Lives remaining: 2
print(check_game_status()) # Output: Playing - Score: 150, Lives: 2Cet exemple montre un usage approprié de global : plusieurs fonctions doivent modifier un état de jeu partagé. Cependant, notez que check_game_status() n’a pas besoin de global parce qu’elle ne fait que lire les variables.
Pourquoi global doit être utilisé avec précaution
Même si global est parfois nécessaire, en abuser peut rendre le code plus difficile à comprendre et à maintenir. Voici pourquoi :
Problème 1 : dépendances cachées
Quand des fonctions modifient des variables globales, ce qui change n’est pas évident depuis l’appel de la fonction :
total = 0
def add_to_total(value):
global total
total += value
# Que fait cette fonction ? Vous ne pouvez pas le savoir sans lire son code
add_to_total(10)Comparez cela à une fonction qui renvoie une valeur :
def add_to_total(current_total, value):
return current_total + value
total = 0
total = add_to_total(total, 10) # Clair : total est mis à jourLa deuxième version rend explicite le fait que total est modifié.
Problème 2 : tester devient plus difficile
Les fonctions qui modifient l’état global sont plus difficiles à tester parce que vous devez initialiser et réinitialiser des variables globales :
# Difficile à tester - dépend d'un état global
score = 0
def add_score(points):
global score
score += points
# Chaque test doit réinitialiser score
# Test 1
score = 0
add_score(10)
assert score == 10
# Test 2 - il faut réinitialiser score à nouveau
score = 0
add_score(20)
assert score == 20Problème 3 : les fonctions ne sont pas réutilisables
Les fonctions qui dépendent de variables globales spécifiques ne peuvent pas être facilement réutilisées dans d’autres programmes :
# Cette fonction ne fonctionne que s'il existe une variable globale nommée 'inventory'
inventory = []
def add_item(item):
global inventory
inventory.append(item)Meilleures alternatives à global
Dans de nombreux cas, vous pouvez éviter global en utilisant des valeurs de retour et des paramètres :
Au lieu de modifier l’état global :
# Utiliser global (moins idéal)
balance = 1000
def withdraw(amount):
global balance
if amount <= balance:
balance -= amount
return True
return False
withdraw(100)
print(balance) # Output: 900Utiliser des valeurs de retour :
# Utiliser des valeurs de retour (mieux)
def withdraw(balance, amount):
if amount <= balance:
return balance - amount, True
return balance, False
balance = 1000
balance, success = withdraw(balance, 100)
print(balance) # Output: 900La deuxième version est plus flexible, plus testable et plus réutilisable.
Quand global est réellement approprié
Il existe des usages légitimes de global :
- Une configuration qui a réellement besoin d’être globale :
# Paramètres à l'échelle de l'application
debug_mode = False
log_level = "INFO"
def enable_debug():
global debug_mode, log_level
debug_mode = True
log_level = "DEBUG"- Des compteurs pour le débogage ou des statistiques :
# Suivre les appels de fonction pour le débogage
_function_call_count = 0
def tracked_function():
global _function_call_count
_function_call_count += 1
# ... reste de la fonctionPoints clés à propos de global
- Utilisez
globaluniquement lorsque vous devez réellement modifier un état au niveau du module - Préférez plutôt renvoyer des valeurs et utiliser des paramètres
- Lorsque vous utilisez
global, documentez pourquoi c’est nécessaire - Demandez-vous si votre conception pourrait être améliorée pour éviter
global - Rappelez-vous : lire des variables globales ne nécessite pas le mot-clé
global— seule leur modification le nécessite
21.4) Utiliser nonlocal pour modifier des variables dans des fonctions englobantes
Lorsque vous avez des fonctions imbriquées, vous pourriez avoir besoin de modifier une variable issue de la portée d’une fonction englobante. Le mot-clé nonlocal sert à cela — il est comme global, mais pour les portées des fonctions englobantes plutôt que pour la portée globale.
Le problème : modifier des variables englobantes
Tout comme l’affectation crée des variables locales par défaut, le même problème se produit avec les portées englobantes :
def outer():
count = 0 # Variable dans la portée de outer
def inner():
# AVERTISSEMENT : ceci crée une NOUVELLE variable locale nommée count - uniquement pour la démonstration
# PROBLÈME : tentative de lecture de count avant de lui affecter une valeur localement
count = count + 1 # UnboundLocalError!
print(count)
inner()
# outer() # This call results in UnboundLocalErrorPython voit l’affectation à count dans inner() et la traite comme une variable locale. Mais nous essayons de la lire avant de lui affecter une valeur localement, ce qui provoque une erreur.
La solution : le mot-clé nonlocal
Le mot-clé nonlocal dit à Python : « Cette variable n’est pas locale — cherchez-la dans la portée de la fonction englobante et utilisez celle-là. »
def outer():
count = 0 # Variable dans la portée de outer
def inner():
nonlocal count # Utiliser le count de la portée de outer
count = count + 1
print(f"Count in inner: {count}")
print(f"Count before: {count}") # Output: Count before: 0
inner() # Output: Count in inner: 1
print(f"Count after: {count}") # Output: Count after: 1
outer()Désormais, inner() peut modifier la variable count de la portée de outer(). Le changement persiste après le retour de inner() parce que nous modifions la variable réelle dans la portée englobante.
Pourquoi nonlocal est utile : des fonctions qui se souviennent d’un état
Le mot-clé nonlocal permet un pattern puissant où les fonctions internes peuvent maintenir et modifier un état issu de leur portée englobante. Nous apprendrons en détail les closures et les fonctions fabrique au Chapitre 23, mais pour l’instant, comprenez que nonlocal permet aux fonctions internes de modifier des variables provenant de portées englobantes.
Voici un exemple simple montrant comment nonlocal fonctionne :
def create_counter():
count = 0 # Cette variable est dans la portée englobante pour increment
def increment():
nonlocal count # Modifier count depuis la portée englobante
count += 1
return count
return increment # Renvoyer la fonction interne
# Créer un compteur
counter1 = create_counter()
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
# Créer un autre compteur indépendant
counter2 = create_counter()
print(counter2()) # Output: 1
print(counter2()) # Output: 2Chaque appel à create_counter() crée une nouvelle variable count et une nouvelle fonction increment() qui peut modifier ce count spécifique en utilisant nonlocal.
nonlocal vs global
Il est important de comprendre la différence :
x = "global"
def outer():
x = "enclosing"
def use_global():
global x # Fait référence au x global
print(f"use_global sees: {x}") # Output: use_global sees: global
def use_nonlocal():
nonlocal x # Fait référence au x de outer
print(f"use_nonlocal sees: {x}") # Output: use_nonlocal sees: enclosing
use_global()
use_nonlocal()
outer()globalfait toujours référence à la portée au niveau du modulenonlocalfait référence à la portée de la fonction englobante la plus proche
Quand vous ne pouvez pas utiliser nonlocal
Le mot-clé nonlocal ne fonctionne qu’avec les portées des fonctions englobantes. Vous ne pouvez pas l’utiliser pour :
- La portée globale (utilisez
globalà la place) :
x = "global"
def func():
nonlocal x # SyntaxError: no binding for nonlocal 'x' found
x = "modified"- Des variables qui n’existent dans aucune portée englobante :
def outer():
def inner():
nonlocal count # SyntaxError: no binding for nonlocal 'count' foundPoints clés à propos de nonlocal
- Utilisez
nonlocalpour modifier des variables provenant des portées de fonctions englobantes nonlocalcherche dans les portées des fonctions englobantes, pas dans la portée globale- Lire des variables englobantes ne nécessite pas
nonlocal— seule leur modification le nécessite nonlocalpermet des patterns puissants pour créer des fonctions avec un état privé- Nous en apprendrons davantage sur les closures et les fonctions fabrique au Chapitre 23
Le mot-clé nonlocal est particulièrement utile pour créer des fonctions qui maintiennent un état privé, comme nous l’avons vu avec l’exemple de compteur.
21.5) Supprimer des noms (pas des objets) avec del et ce que cela signifie
Parfois, vous devez retirer une variable de l’espace de noms de votre programme — par exemple pour libérer de la mémoire dans des programmes de longue durée, nettoyer des variables temporaires, ou supprimer des entrées de collections. L’instruction del de Python gère ces tâches, mais il est important de comprendre exactement ce qu’elle fait et ne fait pas.
L’instruction del en Python est souvent mal comprise. Elle ne supprime pas des objets — elle supprime des noms (liaisons de variables). Comprendre cette distinction est crucial pour comprendre comment Python gère la mémoire et les références.
Ce que del fait réellement
L’instruction del supprime un nom de la portée actuelle :
x = 42
print(x) # Output: 42
del x
# print(x) # NameError: name 'x' is not definedAprès del x, le nom x n’existe plus dans la portée actuelle. Si vous essayez de l’utiliser, Python lève une NameError parce que le nom n’est plus défini.
Supprimer des noms vs supprimer des objets
Voici l’idée clé : del supprime le nom, pas nécessairement l’objet auquel le nom fait référence :
# Créer une liste et deux noms qui y font référence
original = [1, 2, 3]
reference = original # Les deux noms font référence à la même liste
print(original) # Output: [1, 2, 3]
print(reference) # Output: [1, 2, 3]
# Supprimer un des noms
del original
# La liste existe toujours car 'reference' y fait encore référence
print(reference) # Output: [1, 2, 3]
# print(original) # NameError: name 'original' is not definedLa liste [1, 2, 3] continue d’exister parce que reference y fait encore référence. Supprimer original n’a fait que retirer ce nom — cela n’a pas supprimé l’objet liste lui-même.
Quand les objets sont réellement supprimés
Python supprime automatiquement les objets lorsqu’ils ne sont plus référencés par aucun nom. Cela s’appelle le ramasse-miettes (garbage collection) :
data = [1, 2, 3] # La liste est créée, 'data' y fait référence
del data # Le nom 'data' est supprimé
# Maintenant la liste n'a plus de références, donc Python finira par la supprimer
# (Cela se fait automatiquement - vous n'avez rien à faire)Lorsque nous supprimons data, la liste [1, 2, 3] n’a plus de références restantes, donc le ramasse-miettes de Python finira par récupérer la mémoire. Mais cela se fait automatiquement — vous ne contrôlez pas quand cela arrive.
Supprimer des éléments de collections
L’instruction del peut aussi retirer des éléments de collections, mais c’est fondamentalement différent de supprimer des noms. Lorsque vous utilisez del avec un index ou une tranche, vous modifiez la collection elle-même, et non la suppression d’un nom.
C’est une distinction importante : quand vous écrivez del numbers[2], vous appelez une méthode spéciale sur l’objet liste pour retirer un élément. Le nom numbers existe toujours et fait toujours référence au même objet liste — la liste a simplement moins d’éléments maintenant.
# Supprimer des éléments de liste par index
numbers = [10, 20, 30, 40, 50]
del numbers[2] # Retirer l'élément à l'index 2
print(numbers) # Output: [10, 20, 40, 50]
# Supprimer des tranches de liste
numbers = [10, 20, 30, 40, 50]
del numbers[1:3] # Retirer les éléments de l'index 1 à 3 (exclus)
print(numbers) # Output: [10, 40, 50]
# Supprimer des entrées de dictionnaire
person = {'name': 'Alice', 'age': 30, 'city': 'Boston'}
del person['age']
print(person) # Output: {'name': 'Alice', 'city': 'Boston'}