Portée et défauts mutables
Où un nom vit et qui peut le voir, la différence entre lire et écrire un nom extérieur, UnboundLocalError, et le piège le plus célèbre de Python — la liste vide comme défaut.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
La liste « quand ça casse » du chapitre dix-neuf a laissé une ligne sans réponse : « Une variable à l'intérieur de la fonction n'est pas visible à l'extérieur — c'est normal, et pourquoi est le sujet du chapitre vingt-et-un. »
C'est ce chapitre.
def add_tax(price):
tax = price * 0.15
return price + tax
print(add_tax(100))
print(tax)115.0
NameError: name 'tax' is not defined. Did you mean: 'max'?La première ligne a marché, donc tax a dû exister. Et à l'extérieur, elle n'existe pas. (Avant la ligne d'erreur, Python imprime aussi quelques lignes de traceback ; dans ce chapitre, seule la dernière ligne est montrée.)
C'est une protection plutôt qu'une limitation. Dans un programme avec cinquante fonctions, si chaque nom interne fuyait vers l'extérieur, les variables total de deux fonctions s'écraseraient mutuellement — et personne ne s'en apercevrait. Les noms à l'intérieur d'une fonction restent à l'intérieur, donc écrire une fonction ne nécessite pas de connaître les noms des quarante-neuf autres.
Ce chapitre couvre cette règle, et une de ses conséquences qui est le piège le plus célèbre de Python.
À la fin de ce chapitre vous pourrez
- Dire où un nom « vit » et qui peut le voir
- Lire un nom extérieur depuis l'intérieur d'une fonction, et dire pourquoi vous ne pouvez pas en écrire un
- Lire un
UnboundLocalErroret dire ce qui s'est passé - Expliquer pourquoi une liste passée à une fonction peut revenir modifiée
- Reconnaître et éviter le piège le plus célèbre de Python : le défaut mutable
Prérequis : Arguments de fonction.
À l'intérieur reste à l'intérieur
Chaque nom créé à l'intérieur d'une fonction — paramètres inclus — ne vit que pour cet appel. Quand la fonction se termine, les noms disparaissent.
Cela s'appelle la portée : la région dans laquelle un nom est connu.
Lire un nom extérieur fonctionne
RATE = 0.15
def add_tax(price):
return price + price * RATE
print(add_tax(100))115.0Il n'y a pas de RATE à l'intérieur de la fonction, donc Python regarde à l'extérieur et en trouve un. Lire un nom extérieur depuis l'intérieur est correct.
Le nom en majuscules est une convention plutôt qu'une règle Python — il dit au lecteur « ceci est une constante, elle ne change pas ». Utilisé ainsi, un nom extérieur est à son plus sûr.
Mais en écrire un ne marche pas
count = 10
def bump():
count = 99
return count
print(bump())
print(count)99
10Le count = 99 intérieur n'a jamais touché le count extérieur. Il a créé un nouveau nom local qui ne vit qu'à l'intérieur de la fonction et a masqué l'extérieur pour la durée.
C'est la règle, et elle est délibérée : une fonction ne peut pas changer quelque chose à l'extérieur par accident.
Et de là sort une erreur d'allure étrange :
count = 10
def bump():
count = count + 1
return count
print(bump())UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueLa ligne se lit comme si elle prenait le count extérieur et ajoutait un. Mais Python inspecte la fonction entière avant de l'exécuter, voit que quelque chose assigne à count, et décide que count est local à cette fonction du début à la fin.
Puis, en évaluant count + 1 à droite, il cherche le count local, qui n'a pas encore reçu de valeur — et s'arrête.
Le message est donc inhabituellement précis : le nom est local, et rien n'y a encore été associé.
La correction est presque toujours la même, et ce n'est pas global : prenez la valeur comme argument et return le résultat.
def bump(count):
return count + 1
count = bump(count)Python a bien un mot-clé global, et ce cours ne l'enseigne pas, parce que le problème pour lequel les gens l'utilisent a une meilleure réponse. Une fonction qui change l'état extérieur ne peut pas être comprise depuis l'endroit où elle est appelée — et c'est la définition d'un code difficile à tester et difficile à faire confiance.
Mais l'objet que vous avez passé peut changer
La portée protège les noms, pas les objets. La distinction est subtile et importante.
def add_item(items):
items.append("pen")
return items
basket = []
add_item(basket)
add_item(basket)
print(basket)['pen', 'pen']Le basket extérieur a changé, même si le return n'a pas été récupéré.
La raison est celle du chapitre douze : le nom items est nouveau, mais il pointe vers la même liste. append modifie cette liste, et les deux noms montrent le changement.
Comparez :
def replace(items):
items = ["new"]
return items
basket = ["old"]
result = replace(basket)
print(basket)
print(result)['old']
['new']Ici items = [...] a assigné une nouvelle valeur, pointant le nom local vers une liste différente. Le basket extérieur est là où il était.
La règle en une ligne : modifier l'objet (append, sort, [i] =) est visible à l'extérieur ; pointer le nom vers un autre objet (=) ne l'est pas.
Le piège le plus célèbre de Python
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))
print(add_item("ink"))['pen']
['pen', 'bag']
['pen', 'bag', 'ink']Trois appels séparés, et chacun garde ce que les précédents ont laissé.
Comprenez la raison et vous ne l'oublierez jamais : la valeur par défaut est évaluée une fois, quand la ligne def s'exécute — pas quand la fonction est appelée. Cette liste vide est une seule liste attachée à la fonction, et chaque appel la partage.
La correction est conventionnelle et simple :
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))['pen']
['bag']None est immuable, donc le partager ne coûte rien, et la nouvelle liste est créée fraîchement à chaque appel.
Comme règle : n'écrivez jamais une liste, un dictionnaire ou un ensemble comme valeur par défaut. Les nombres, le texte, True/False et None sont sûrs, précisément parce qu'ils ne peuvent pas être changés.
Pourquoiis Noneet pas== None?isdemande « est-ce le tout même objet ? », et il n'y a qu'un seulNonedans tout un programme.==teste l'égalité, que certains types définissent de façon surprenante. PourNone,isest à la fois conventionnel et fiable.
Un exemple complet
basket.py :
# Where a name lives decides who can see it and who can change it
TAX_RATE = 0.15
def line_total(price, quantity):
"""Pure: takes values, returns a value, touches nothing outside."""
return round(price * quantity * (1 + TAX_RATE), 2)
def add_line(basket, name, price, quantity):
"""Impure on purpose: it changes the basket it was handed."""
basket.append({"name": name, "total": line_total(price, quantity)})
return basket
def summarise(basket, note=None):
"""`None` as the default, so no list or dict is shared between calls."""
notes = [] if note is None else [note]
total = sum(line["total"] for line in basket)
notes.append(f"{len(basket)} lines, {total:.2f} in total")
return notes
shopping = []
add_line(shopping, "pen", 15.0, 3)
add_line(shopping, "bag", 850.0, 1)
for line in shopping:
print(f"{line['name']:<6} {line['total']:>8.2f}")
print()
for note in summarise(shopping, "morning order"):
print(note)
print()
print("basket outside the function:", len(shopping), "lines")pen 51.75
bag 977.50
morning order
2 lines, 1029.25 in total
basket outside the function: 2 linesTrois fonctions avec trois relations différentes au monde extérieur, et c'est ce qu'il faut regarder.
line_total ne touche à rien. Elle prend des valeurs et renvoie une valeur. Elle lit TAX_RATE et ne la change jamais. Une fonction comme ça s'appelle pure — la même entrée donne toujours la même sortie, sans effet de bord nulle part. C'est le genre le plus facile à tester et le plus difficile à rater.
add_line change quelque chose à l'extérieur exprès. On lui donne une liste et elle y ajoute — et la dernière ligne prouve que le shopping extérieur contient vraiment deux lignes. Il n'y a rien de mal à cela, tant que le nom le dit. Lire add_line vous dit que quelque chose va être ajouté.
summarise utilise None comme défaut. Écrit note=[], chaque appel partagerait une liste, et le deuxième rapport emporterait la note du premier.
Quand ça casse
NameError: name 'tax' is not defined Un nom de l'intérieur d'une fonction a été utilisé à l'extérieur. return ce qui est nécessaire à l'extérieur.
UnboundLocalError: cannot access local variable ... where it is not associated with a value Un nom est assigné à l'intérieur de la fonction et lu avant cette assignation. Python a décidé qu'il était local. Prenez la valeur comme argument et return le résultat.
Ma liste a changé après que je l'ai passée à une fonction C'est le comportement attendu — le nom est nouveau, la liste est la même. Pour l'éviter, écrivez items = items.copy() en haut de la fonction, ou construisez et return une nouvelle liste.
La fonction se souvient de ses résultats précédents Un défaut mutable. Pas def f(x, acc=[]) mais def f(x, acc=None), avec if acc is None: acc = [] à l'intérieur.
J'ai utilisé le même nom dans deux fonctions — est-ce un problème ? Ce n'en est pas un, et c'est tout le but de la portée. Les variables total de deux fonctions sont complètement séparées.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Une valeur est assignée à count à l'intérieur de la fonction. Que s'affichent les deux lignes ?
count = 10
def bump():
count = 99
return count
print(bump())
print(count)- A10 10
- B99 10
- CUn `UnboundLocalError`
- D99 99
Il n'y a pas de return, pourtant la liste extérieure est imprimée. Qu'est-ce qui s'affiche ?
def add_item(items):
items.append("pen")
basket = []
add_item(basket)
add_item(basket)
print(basket)- A[]
- B['pen']
- CNone
- D['pen', 'pen']
Deux appels séparés, avec une liste vide comme défaut. Qu'est-ce qui s'imprime ?
def collect(item, box=[]):
box.append(item)
return box
print(collect("pen"))
print(collect("bag"))- A['pen', 'bag'] ['pen', 'bag']
- B['pen'] ['bag']
- C['pen'] ['pen', 'bag']
- D['pen'] ['pen']
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
Écrivez un fichier appelé cart.py avec trois fonctions :
line_total(price, quantity, tax_rate=0.15)— pure, ne touchant à rienadd_to(cart, name, price, quantity)— ajoute au panier qu'on lui donnereceipt(cart, header=None)— renvoie une liste de lignes, inventant un en-tête quand aucun n'est donné
Puis construisez un panier vide, ajoutez quelques choses, et imprimez le reçu.
Enfin, trois expériences, toutes valant d'être vues de vos propres yeux :
- Changez le défaut de
receiptenheader=[], puis appelez la fonction deux fois et imprimez les deux résultats. Que voyez-vous la deuxième fois ? - Ajoutez
cart = cart.copy()comme première ligne deadd_to, puis imprimez le panier extérieur. Combien de choses y a-t-il, et pourquoi ? - Assignez
TAX_RATE = 0.2à l'intérieur d'une fonction, puis imprimezTAX_RATEà l'extérieur. Quelle valeur voyez-vous ?
Les trois réponses ensemble sont tout le chapitre : quels noms vivent où, et quels changements atteignent l'extérieur.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz