8.1 Mal comprendre les règles de la portée en Python
La portée en Python est basée sur la règle dite LEGB, qui est un acronyme :
-
Local
(noms attribués de quelque manière que ce soit à l'intérieur d'une fonction (def
oulambda
), et non déclarés globalement dans cette fonction) ; Enclosing
(noms dans la portée locale de toute fonction englobante (def
oulambda
), de l'intérieur vers l'extérieur) ;Global
(noms attribués au niveau supérieur du fichier module, ou en exécutant l'instructionglobal
dans undef
à l'intérieur du fichier) ;-
Built-in
(noms pré-définis dans le module des noms intégrés :open
,range
,SyntaxError
, et d'autres).
Ça semble assez simple, non ?
Cependant, il y a certaines subtilités dans la façon dont cela fonctionne en Python, ce qui nous conduit à un problème complexe de programmation en Python. Considérons l'exemple suivant :
x = 10
def foo():
x += 1
print(x)
foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment
Quel est le problème ?
L'erreur ci-dessus survient parce que, lorsque tu assignes une valeur à une variable dans la portée
, Python
considère automatiquement qu'elle est locale
à cette portée et masque toute variable avec
le même nom dans toute
portée supérieure.
Ainsi, beaucoup sont surpris lorsqu'ils reçoivent un UnboundLocalError
dans un code qui fonctionnait auparavant, lorsqu'il
est modifié en ajoutant un opérateur d'assignation quelque part dans le corps d'une fonction.
Cette particularité embrouille particulièrement les développeurs lors de l'utilisation des listes. Considérons l'exemple suivant :
lst = [1, 2, 3]
def foo1():
lst.append(5) # Ça fonctionne bien...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... mais ça plante !
foo2()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment
Pourquoi foo2
plante alors que foo1
fonctionne bien ?
La réponse est la même que pour l'exemple précédent, mais, de l'avis général, la situation est plus subtile ici. foo1
ne
fait pas d'assignation à lst
, alors que foo2
le fait. Rappelons que lst += [5]
n'est en réalité qu'un
raccourci pour lst = lst + [5]
, on voit que nous essayons d'assigner une valeur à lst
(donc Python suppose
qu'il est dans une portée locale). Cependant, la valeur que nous voulons assigner à lst
est basée sur
lst
lui-même (encore une fois, maintenant supposé être dans une portée locale), qui n'a pas encore été
définie. Et nous obtenons une erreur.
8.2 Modifier une liste pendant son itération
Le problème dans le morceau de code suivant devrait être assez évident :
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
for i in range(len(numbers)):
if odd(numbers[i]):
del numbers[i] # MAUVAIS : Suppression d'un élément d'une liste pendant l'itération
Traceback (most recent call last):
File "", line 2, in
IndexError: list index out of range
Supprimer un élément d'une liste ou d'un tableau pendant son itération est un problème Python bien connu de tout développeur de software expérimenté. Mais, bien que l'exemple ci-dessus puisse être assez évident, même les développeurs expérimentés peuvent tomber dans ce piège dans un code beaucoup plus complexe.
Heureusement, Python inclut une série de paradigmes de programmation élégants qui, lorsqu'ils sont utilisés correctement, peuvent conduire à une simplification et une optimisation significative du code. Un agrément supplémentaire de ceci est que dans un code plus simple, la probabilité de tomber sur l'erreur de suppression accidentelle d'un élément de liste pendant son itération est considérablement réduite.
L'un de ces paradigmes est les comprehension de liste. De plus, comprendre comment fonctionnent les comprehension de liste est particulièrement utile pour éviter ce problème particulier, comme montré dans cette implémentation alternative du code ci-dessus, qui fonctionne parfaitement :
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # nous ne choisissons que les nouveaux éléments
print(numbers)
# [0, 2, 4, 6, 8]
Important !
Ici, il n'y a pas d'assignation d'un nouvel objet liste. Utiliser
numbers[:]
est une assignation
groupée de nouvelles valeurs à tous les éléments de la liste.
8.3 Ne pas comprendre comment Python lie les variables dans les closures
Considérons l'exemple suivant :
def create_multipliers():
return [lambda x: i * x for i in range(5)] #Retourne une liste de fonctions !
for multiplier in create_multipliers():
print(multiplier(2))
Tu pourrais t'attendre à la sortie suivante :
0
2
4
6
8
Mais en fait, tu obtiendras ceci :
8
8
8
8
8
Surprise !
Cela se produit en raison de la liaison tardive en Python, qui signifie que les valeurs des variables utilisées dans les closures sont recherchées au moment de l'appel de la fonction interne.
Ainsi, dans le code ci-dessus, chaque fois que l'une des fonctions retournées est appelée, la valeur
i
est recherchée dans la portée environnante au moment de son appel (et à ce moment-là, la boucle est déjà terminée, donc i
a déjà
été assignée au résultat final - la valeur 4).
La solution à ce problème courant en Python serait :
def create_multipliers():
return [lambda x, i = i : i * x for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 0
# 2
# 4
# 6
# 8
Et voilà ! Nous utilisons ici des arguments par défaut pour générer des fonctions anonymes afin d'obtenir le comportement souhaité. Certains qualifieraient cette solution d'élégante. D'autres de subtile. D'autres encore détestent ce genre de trucs. Mais si tu es un développeur Python, il est important de comprendre cela de toute façon.
GO TO FULL VERSION