Complément - niveau basique¶
Une astuce¶
Dans ce complément, nous allons beaucoup jouer avec le fait qu’une variable soit définie ou non. Pour nous simplifier la vie, et surtout rendre les cellules plus indépendantes les unes des autres si vous devez les rejouer, nous allons utiliser la formule un peu magique suivante :
# on détruit la variable i si elle existe
if 'i' in locals():
del iqui repose d’une part sur l’instruction del que nous avons déjà vue, et sur la fonction built-in locals que nous verrons plus tard ; cette formule a l’avantage qu’on peut l’exécuter dans n’importe quel contexte, que i soit définie ou non.
Une variable de boucle reste définie au-delà de la boucle¶
Une variable de boucle est définie (assignée) dans la boucle et reste visible une fois la boucle terminée. Le plus simple est de le voir sur un exemple :
# La variable 'i' n'est pas définie
try:
i
except NameError as e:
print('OOPS', e)OOPS name 'i' is not defined
# si à présent on fait une boucle
# avec i comme variable de boucle
for i in [0]:
pass
# alors maintenant i est définie
i0On dit que la variable fuite (en anglais “leak”), dans ce sens qu’elle continue d’exister au delà du bloc de la boucle à proprement parler.
On peut être tenté de tirer profit de ce trait, en lisant la valeur de la variable après la boucle ; l’objet de ce complément est de vous inciter à la prudence, et d’attirer votre attention sur certains points qui peuvent être sources d’erreur.
Attention aux boucles vides¶
Tout d’abord, il faut faire attention à ne pas écrire du code qui dépende de ce trait si la boucle peut être vide. En effet, si la boucle ne s’exécute pas du tout, la variable n’est pas affectée et donc elle n’est pas définie. C’est évident, mais ça peut l’être moins quand on lit du code réel, comme par exemple :
# on détruit la variable i si elle existe
if 'i' in locals():
del i# une façon très scabreuse de calculer la longueur de l
def length(l):
for i, x in enumerate(l):
pass
return i + 1
length([1, 2, 3])3Ça a l’air correct, sauf que :
# ceci provoque une UnboundLocalError
length([])---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Cell In[6], line 2
1 # ceci provoque une UnboundLocalError
----> 2 length([])
Cell In[5], line 5, in length(l)
3 for i, x in enumerate(l):
4 pass
----> 5 return i + 1
UnboundLocalError: cannot access local variable 'i' where it is not associated with a valueCe résultat mérite une explication. Nous allons voir très bientôt l’exception UnboundLocalError, mais pour le moment sachez qu’elle se produit lorsqu’on a dans une fonction une variable locale et une variable globale de même nom. Alors, pourquoi l’appel length([1, 2, 3]) retourne-t-il sans encombre, alors que pour l’appel length([]) il y a une exception ? Cela est lié à la manière dont python détermine qu’une variable est locale.
Une variable est locale dans une fonction si elle est assignée dans la fonction explicitement (avec une opération d’affectation) ou implicitement (par exemple avec une boucle for comme ici) ; nous reviendrons sur ce point un peu plus tard. Mais pour les fonctions, pour une raison d’efficacité, une variable est définie comme locale à la phase de pré-compilation, c’est-à-dire avant l’exécution du code. Le pré-compilateur ne peut pas savoir quel sera l’argument passé à la fonction, il peut simplement savoir qu’il y a une boucle for utilisant la variable i, il en conclut que i est locale pour toute la fonction.
Lors du premier appel, on passe une liste à la fonction, liste qui est parcourue par la boucle for. En sortie de boucle, on a bien une variable locale i qui vaut 3. Lors du deuxième appel par contre, on passe une liste vide à la fonction, la boucle for ne peut rien parcourir, donc elle termine immédiatement. Lorsque l’on arrive à la ligne return i + 1 de la fonction, la variable i n’a pas de valeur (on doit donc chercher i dans le module), mais i a été définie par le pré-compilateur comme étant locale, on a donc dans la même fonction une variable i locale et une référence à une variable i globale, ce qui provoque l’exception UnboundLocalError.
Comment faire alors ?¶
Utiliser une autre variable¶
La première voie consiste à déclarer une variable externe à la boucle et à l’affecter à l’intérieur de la boucle, c’est-à-dire :
# on veut chercher le premier de ces nombres qui vérifie une condition
candidates = [3, -15, 1, 8]
# pour fixer les idées disons qu'on cherche un multiple de 5, peu importe
def checks(candidate):
return candidate % 5 == 0# plutôt que de faire ceci
for item in candidates:
if checks(item):
break
print('trouvé solution', item)trouvé solution -15
# il vaut mieux faire ceci
solution = None
for item in candidates:
if checks(item):
solution = item
break
print('trouvé solution', solution)trouvé solution -15
Au minimum initialiser la variable¶
Au minimum, si vous utilisez la variable de boucle après la boucle, il est vivement conseillé de l’initialiser explicitement avant la boucle, pour vous prémunir contre les boucles vides, comme ceci :
# la fonction length de tout à l'heure
def length1(l):
for i, x in enumerate(l):
pass
return i + 1# une version plus robuste
def length2(l):
# on initialise i explicitement
# pour le cas où l est vide
i = -1
for i, x in enumerate(l):
pass
# comme cela i est toujours déclarée
return i + 1# comme ci-dessus: UnboundLocalError
length1([])---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Cell In[12], line 2
1 # comme ci-dessus: UnboundLocalError
----> 2 length1([])
Cell In[10], line 5, in length1(l)
3 for i, x in enumerate(l):
4 pass
----> 5 return i + 1
UnboundLocalError: cannot access local variable 'i' where it is not associated with a valuelength2([])0Les compréhensions¶
Notez bien que par contre, les variables de compréhension ne fuient pas (contrairement à ce qui se passait en Python 2) :
# on détruit la variable i si elle existe
if 'i' in locals():
del i# en Python 3, les variables de compréhension ne fuitent pas
[i**2 for i in range(3)][0, 1, 4]# ici i est à nouveau indéfinie
try:
i
except NameError as e:
print("OOPS", e)OOPS name 'i' is not defined