Cours de Python / Comprendre la portée des noms, les alias et les objets modifiables
La portée détermine où Python cherche un nom ; la mutabilité détermine quels changements sont visibles à travers plusieurs noms partageant un objet. Distinguer une réaffectation de nom d’une modification de l’objet permet de comprendre pourquoi des données peuvent changer après un appel de fonction.
Séparer les noms locaux des noms extérieurs
Un nom auquel une fonction affecte une valeur est normalement local à cette fonction, sauf déclaration explicite. Lire une valeur extérieure et modifier la liaison d’un nom extérieur sont deux opérations distinctes. La recherche ordinaire des noms dans une fonction suit les portées locale, des fonctions englobantes, globale du module puis des noms natifs : on résume souvent cet ordre par LEGB, pour Local, Enclosing, Global, Builtins.
global désigne une liaison au niveau du module ; nonlocal désigne une liaison déjà présente dans une fonction englobante. Avant d’ajouter ces déclarations, vérifiez si des arguments et une valeur de retour rendraient le flux de données plus clair. Une modification de liste par sa méthode append ne nécessite pas de réaffecter le nom extérieur qui la référence.
La documentation des portées et espaces de nommage Python explique ces relations entre noms et objets. Le partage d’une liste entre appels apparaît aussi avec les valeurs par défaut modifiables.
Observer les listes partagées et copiées
Python 3 et sa bibliothèque standard suffisent. Enregistrez le premier programme complet dans scope.py, en UTF-8, dans un dossier d’exercice. Avec l’environnement virtuel du projet activé, ouvrez le terminal dans ce dossier puis lancez python scope.py. Sur macOS ou Ubuntu hors environnement virtuel, utilisez python3 scope.py si votre interpréteur porte ce nom.
def add_fee(items, fee):
items.append(fee)
original = [3500, 8000]
alias = original
copy = original.copy()
add_fee(alias, 500)
print(original)
print(copy)
nested = [[1], [2]]
shallow = nested.copy()
shallow[0].append(9)
print(nested)
Le résultat attendu est :
[3500, 8000, 500]
[3500, 8000]
[[1, 9], [2]]
alias = original crée une autre référence à la même liste, sans la copier. Le paramètre items référence cet objet pendant l’appel ; son append ajoute donc 500 à la liste visible par original et alias. En revanche, original.copy() a créé auparavant une nouvelle liste extérieure : la variable copy ne voit pas cet ajout.
Le nom copy est ici une simple variable de liste, pas le module standard du même nom. Si vous ajoutez ultérieurement import copy, choisissez un autre nom de variable, par exemple copied_items, pour éviter de masquer le module.
Comparer copie superficielle et résolution des noms
Une copie superficielle construit un nouveau conteneur extérieur tout en conservant les références de ses éléments. Dans la partie nested, shallow est donc une autre liste extérieure, mais shallow[0] et nested[0] désignent la même liste intérieure. Lui ajouter neuf est visible des deux côtés. Remplacer seulement un élément extérieur, par exemple en affectant une nouvelle liste à shallow[0], n’aurait pas le même effet sur nested.
La fonction copy.deepcopy() peut copier récursivement des structures, mais tous les objets ne sont pas copiables et certains partages sont intentionnels. Elle ne constitue pas un moyen universel de dupliquer une connexion, un fichier ouvert ou tout état externe. Les règles du module copy détaillent ces limites. Choisissez le niveau d’indépendance nécessaire aux données.
Pour isoler la question des noms, enregistrez le second programme complet dans scope_names.py, un fichier distinct du premier, puis exécutez python scope_names.py ou python3 scope_names.py selon votre environnement.
value = 10
def local_read():
value = 20
return value
def broken():
value += 1
return value
print(local_read(), value)
try:
broken()
except UnboundLocalError:
print("local name not initialized")
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter())
Le résultat attendu est :
20 10
local name not initialized
1 2
local_read affecte vingt à son nom local value, tandis que le value du module reste dix. Dans broken, value += 1 nécessite de lire value puis de le réaffecter ; cette affectation rend le nom local, mais aucune valeur locale n’a encore été établie au moment de la lecture. L’appel lève donc une UnboundLocalError, capturée ici pour afficher « local name not initialized », c’est-à-dire « nom local non initialisé ».
Dans make_counter, la déclaration nonlocal count permet à increment de réaffecter le count de sa fonction englobante. Les deux appels du même counter renvoient un puis deux. Un nouvel appel de make_counter crée un autre compteur avec son propre état. Les règles officielles de résolution des noms précisent le traitement des variables locales et de cette erreur.
Vérifier le contrat de modification d’une fonction
- Dans
scope.py, ajoutezadd_fee(copy, 100)après l’appeladd_fee(alias, 500), avant les affichages. original reste[3500, 8000, 500], tandis que copy devient[3500, 8000, 100]. Retirez ensuite cet appel. - Remplacez uniquement
items.append(fee)paritems = items + [fee]. Cette opération crée une nouvelle liste et réaffecte le nom local items ; original reste[3500, 8000]. La fonction ne renvoie pas cette nouvelle liste, donc elle n’est pas récupérée par l’appelant. Restaurez append. - Dans
scope_names.py, ajoutez après les affichages un nouvel appelprint(make_counter()()). Il affiche un, car il crée et utilise un compteur indépendant. Retirez cet essai et retrouvez les trois lignes de référence.
Un appelant doit pouvoir savoir si la fonction modifie l’objet reçu. Une variante qui préserve sa liste d’entrée peut créer une nouvelle liste puis la renvoyer ; il faut alors utiliser cette valeur de retour. Vérifiez le contenu de l’entrée autant que le résultat : une valeur de retour correcte ne révèle pas nécessairement une modification indésirable de la source.




