CodeGym /Cours /Python SELF FR /Erreurs standard, partie 2

Erreurs standard, partie 2

Python SELF FR
Niveau 20 , Leçon 2
Disponible

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 ou lambda), et non déclarés globalement dans cette fonction) ;
  • Enclosing (noms dans la portée locale de toute fonction englobante (def ou lambda), de l'intérieur vers l'extérieur) ;
  • Global (noms attribués au niveau supérieur du fichier module, ou en exécutant l'instruction global dans un def à 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.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION