Code Python bogué : les 10 erreurs les plus courantes commises par les développeurs Python

Publié: 2022-03-11

À propos de Python

Python est un langage de programmation interprété, orienté objet et de haut niveau avec une sémantique dynamique. Ses structures de données intégrées de haut niveau, combinées au typage dynamique et à la liaison dynamique, le rendent très attrayant pour le développement rapide d'applications, ainsi que pour une utilisation en tant que langage de script ou de collage pour connecter des composants ou des services existants. Python prend en charge les modules et les packages, encourageant ainsi la modularité du programme et la réutilisation du code.

À propos de cet article

La syntaxe simple et facile à apprendre de Python peut induire en erreur les développeurs Python, en particulier ceux qui débutent dans le langage, en leur faisant passer à côté de certaines de ses subtilités et en sous-estimant la puissance du langage Python diversifié.

Dans cet esprit, cet article présente une liste du "top 10" des erreurs quelque peu subtiles et plus difficiles à détecter qui peuvent mordre même certains développeurs Python plus avancés à l'arrière.

(Remarque : cet article est destiné à un public plus avancé que Common Mistakes of Python Programmers, qui s'adresse davantage à ceux qui découvrent le langage.)

Erreur courante n° 1 : utiliser à mauvais escient des expressions comme valeurs par défaut pour les arguments de fonction

Python vous permet de spécifier qu'un argument de fonction est facultatif en lui fournissant une valeur par défaut . Bien qu'il s'agisse d'une fonctionnalité intéressante du langage, cela peut prêter à confusion lorsque la valeur par défaut est mutable . Par exemple, considérez cette définition de fonction Python :

 >>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar

Une erreur courante consiste à penser que l'argument facultatif sera défini sur l'expression par défaut spécifiée chaque fois que la fonction est appelée sans fournir de valeur pour l'argument facultatif. Dans le code ci-dessus, par exemple, on pourrait s'attendre à ce que l'appel répété de foo() (c'est-à-dire sans spécifier d'argument bar ) renvoie toujours 'baz' , car l'hypothèse serait que chaque fois que foo() est appelé (sans bar argument spécifié) bar est défini sur [] (c'est-à-dire une nouvelle liste vide).

Mais regardons ce qui se passe réellement lorsque vous faites ceci :

 >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]

Hein? Pourquoi continuait-il d'ajouter la valeur par défaut de "baz" à une liste existante à chaque fois que foo() était appelé, plutôt que de créer une nouvelle liste à chaque fois ?

La réponse de programmation Python la plus avancée est que la valeur par défaut d'un argument de fonction n'est évaluée qu'une seule fois, au moment où la fonction est définie. Ainsi, l'argument bar est initialisé à sa valeur par défaut (c'est-à-dire une liste vide) uniquement lorsque foo() est défini pour la première fois, mais les appels à foo() (c'est-à-dire sans qu'un argument bar soit spécifié) continueront à utiliser la même liste pour quelle bar a été initialisée à l'origine.

Pour votre information, une solution de contournement courante pour cela est la suivante :

 >>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]

Erreur courante n° 2 : utiliser des variables de classe de manière incorrecte

Considérez l'exemple suivant :

 >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1

Logique.

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

Oui, encore une fois comme prévu.

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

Qu'est-ce que le $%#!& ?? Nous avons seulement changé Ax . Pourquoi Cx a-t-il aussi changé ?

En Python, les variables de classe sont gérées en interne comme des dictionnaires et suivent ce que l'on appelle souvent l'ordre de résolution de méthode (MRO). Ainsi, dans le code ci-dessus, puisque l'attribut x n'est pas trouvé dans la classe C , il sera recherché dans ses classes de base (seulement A dans l'exemple ci-dessus, bien que Python supporte plusieurs héritages). En d'autres termes, C n'a pas sa propre propriété x , indépendante de A . Ainsi, les références à Cx sont en fait des références à Ax . Cela provoque un problème Python à moins qu'il ne soit géré correctement. En savoir plus sur les attributs de classe en Python.

Erreur courante n° 3 : spécifier des paramètres de manière incorrecte pour un bloc d'exception

Supposons que vous ayez le code suivant :

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range

Le problème ici est que l'instruction except ne prend pas une liste d'exceptions spécifiées de cette manière. Au lieu de cela, dans Python 2.x, la syntaxe except Exception, e est utilisée pour lier l'exception au deuxième paramètre facultatif spécifié (dans ce cas e ), afin de la rendre disponible pour une inspection plus approfondie. Par conséquent, dans le code ci-dessus, l'exception IndexError n'est pas interceptée par l'instruction except ; à la place, l'exception finit par être liée à un paramètre nommé IndexError .

La bonne façon d'intercepter plusieurs exceptions dans une instruction except consiste à spécifier le premier paramètre sous la forme d'un tuple contenant toutes les exceptions à intercepter. De plus, pour une portabilité maximale, utilisez le mot-clé as , car cette syntaxe est prise en charge à la fois par Python 2 et Python 3 :

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>

Erreur courante n° 4 : incompréhension des règles de portée de Python

La résolution de portée Python est basée sur ce que l'on appelle la règle LEGB, qui est un raccourci pour Local, Enclosing, G lobal, B uilt - in . Cela semble assez simple, non? Eh bien, en fait, il y a quelques subtilités dans la façon dont cela fonctionne en Python, ce qui nous amène au problème de programmation Python plus avancé ci-dessous. Considérer ce qui suit:

 >>> 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 se produit car, lorsque vous effectuez une affectation à une variable dans une portée, cette variable est automatiquement considérée par Python comme étant locale à cette portée et masque toute variable portant le même nom dans n'importe quelle portée externe.

Beaucoup sont donc surpris d'obtenir une UnboundLocalError dans le code qui fonctionnait auparavant lorsqu'il est modifié en ajoutant une instruction d'affectation quelque part dans le corps d'une fonction. (Vous pouvez en savoir plus à ce sujet ici.)

Il est particulièrement courant que cela fasse trébucher les développeurs lors de l'utilisation de listes. Considérez l'exemple suivant :

 >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment

Hein? Pourquoi foo2 a-t-il bombardé alors que foo1 fonctionnait bien ?

La réponse est la même que dans le problème de l'exemple précédent mais est certes plus subtile. foo1 ne fait pas d' affectation à lst , alors que foo2 est. En se souvenant que lst += [5] n'est en fait qu'un raccourci pour lst = lst + [5] , nous voyons que nous essayons d' attribuer une valeur à lst (donc présumé par Python être dans la portée locale). Cependant, la valeur que nous cherchons à attribuer à lst est basée sur lst lui-même (encore une fois, maintenant présumé être dans la portée locale), qui n'a pas encore été définie. Boom.

Erreur courante n° 5 : modifier une liste tout en l'itérant

Le problème avec le 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] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range

La suppression d'un élément d'une liste ou d'un tableau lors d'une itération est un problème Python bien connu de tout développeur de logiciel expérimenté. Mais bien que l'exemple ci-dessus puisse être assez évident, même les développeurs avancés peuvent être involontairement mordus par cela dans un code beaucoup plus complexe.

Heureusement, Python intègre un certain nombre de paradigmes de programmation élégants qui, lorsqu'ils sont utilisés correctement, peuvent aboutir à un code considérablement simplifié et rationalisé. Un avantage secondaire de ceci est qu'un code plus simple est moins susceptible d'être mordu par le bogue de suppression accidentelle d'un élément de liste lors d'une itération. Un de ces paradigmes est celui des compréhensions de listes. De plus, les compréhensions de liste sont particulièrement utiles pour éviter ce problème spécifique, comme le montre 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)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]

Erreur courante n° 6 : Confondre la façon dont Python lie les variables dans les fermetures

Considérant l'exemple suivant :

 >>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...

Vous pouvez vous attendre au résultat suivant :

 0 2 4 6 8

Mais vous obtenez en fait :

 8 8 8 8 8

Surprendre!

Cela se produit en raison du comportement de liaison tardive de Python qui indique que les valeurs des variables utilisées dans les fermetures sont recherchées au moment où la fonction interne est appelée. Ainsi, dans le code ci-dessus, chaque fois que l'une des fonctions renvoyées est appelée, la valeur de i est recherchée dans la portée environnante au moment où elle est appelée (et à ce moment-là, la boucle est terminée, donc i déjà reçu son final valeur de 4).

La solution à ce problème Python commun est un peu un hack :

 >>> 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

Voila ! Nous profitons ici des arguments par défaut pour générer des fonctions anonymes afin d'obtenir le comportement souhaité. Certains qualifieraient cela d'élégant. Certains diront que c'est subtil. Certains le détestent. Mais si vous êtes un développeur Python, il est important de comprendre dans tous les cas.

Erreur courante n°7 : créer des dépendances de modules circulaires

Supposons que vous ayez deux fichiers, a.py et b.py , dont chacun importe l'autre, comme suit :

Dans a.py :

 import b def f(): return bx print f()

Et dans b.py :

 import a x = 1 def g(): print af()

Essayons d'abord d'importer a.py :

 >>> import a 1

A très bien fonctionné. Cela vous surprend peut-être. Après tout, nous avons ici une importation circulaire qui devrait probablement poser problème, n'est-ce pas ?

La réponse est que la simple présence d'une importation circulaire n'est pas en soi un problème en Python. Si un module a déjà été importé, Python est suffisamment intelligent pour ne pas essayer de le réimporter. Cependant, selon le moment où chaque module tente d'accéder à des fonctions ou variables définies dans l'autre, vous pouvez effectivement rencontrer des problèmes.

Donc, pour revenir à notre exemple, lorsque nous avons importé a.py , il n'y a eu aucun problème à importer b.py , car b.py ne nécessite rien de a.py à définir au moment où il est importé . La seule référence dans b.py à a est l'appel à af() . Mais cet appel est dans g() et rien dans a.py ou b.py n'invoque g() . Alors la vie est belle.

Mais que se passe-t-il si nous essayons d'importer b.py (sans avoir préalablement importé a.py , c'est-à-dire) :

 >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'

Oh-oh. Ce n'est pas bon! Le problème ici est que, lors du processus d'importation de b.py , il tente d'importer a.py , qui à son tour appelle f() , qui tente d'accéder à bx . Mais bx n'a pas encore été défini. D'où l'exception AttributeError .

Au moins une solution à cela est assez triviale. Modifiez simplement b.py pour importer a.py dans g() :

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

Non quand on l'importe, tout va bien :

 >>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'

Erreur courante n° 8 : nom en conflit avec les modules de la bibliothèque standard Python

L'une des beautés de Python est la richesse des modules de bibliothèque qu'il contient "prêts à l'emploi". Mais par conséquent, si vous ne l'évitez pas consciemment, il n'est pas si difficile de rencontrer un conflit de noms entre le nom de l'un de vos modules et un module portant le même nom dans la bibliothèque standard livrée avec Python (par exemple , vous pourriez avoir un module nommé email.py dans votre code, qui serait en conflit avec le module de bibliothèque standard du même nom).

Cela peut entraîner des problèmes épineux, tels que l'importation d'une autre bibliothèque qui à son tour essaie d'importer la version Python Standard Library d'un module mais, puisque vous avez un module avec le même nom, l'autre paquet importe par erreur votre version au lieu de celle dans la bibliothèque standard Python. C'est là que les mauvaises erreurs Python se produisent.

Il faut donc veiller à éviter d'utiliser les mêmes noms que ceux des modules de la bibliothèque standard Python. Il est beaucoup plus facile pour vous de changer le nom d'un module dans votre package que de déposer une proposition d'amélioration Python (PEP) pour demander un changement de nom en amont et essayer de le faire approuver.

Erreur courante n° 9 : Ne pas résoudre les différences entre Python 2 et Python 3

Considérez le fichier suivant foo.py :

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()

Sur Python 2, cela fonctionne bien :

 $ python foo.py 1 key error 1 $ python foo.py 2 value error 2

Mais maintenant, essayons Python 3 :

 $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment

Que vient-il de se passer ici ? Le "problème" est que, dans Python 3, l'objet exception n'est pas accessible au-delà de la portée du bloc except . (La raison en est que, sinon, il conserverait un cycle de référence avec le cadre de pile en mémoire jusqu'à ce que le ramasse-miettes s'exécute et purge les références de la mémoire. Plus de détails techniques à ce sujet sont disponibles ici).

Une façon d'éviter ce problème consiste à conserver une référence à l'objet exception en dehors de la portée du bloc except afin qu'il reste accessible. Voici une version de l'exemple précédent qui utilise cette technique, produisant ainsi un code compatible avec Python 2 et Python 3 :

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()

Exécuter ceci sur Py3k :

 $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2

Hourra!

(Incidemment, notre Python Hiring Guide traite d'un certain nombre d'autres différences importantes à prendre en compte lors de la migration du code de Python 2 vers Python 3.)

Erreur courante n° 10 : utiliser la méthode __del__ à mauvais escient

Disons que vous aviez ceci dans un fichier appelé mod.py :

 import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)

Et vous avez ensuite essayé de le faire depuis another_mod.py :

 import mod mybar = mod.Bar()

Vous obtiendrez une horrible exception AttributeError .

Pourquoi? Parce que, comme indiqué ici, lorsque l'interpréteur s'arrête, les variables globales du module sont toutes définies sur None . Par conséquent, dans l'exemple ci-dessus, au moment où __del__ est appelé, le nom foo a déjà été défini sur None .

Une solution à ce problème de programmation Python un peu plus avancé consisterait à utiliser atexit.register() à la place. De cette façon, lorsque votre programme a fini de s'exécuter (c'est-à-dire en quittant normalement), vos gestionnaires enregistrés sont lancés avant que l'interpréteur ne soit arrêté.

Avec cette compréhension, un correctif pour le code mod.py ci-dessus pourrait alors ressembler à ceci :

 import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)

Cette implémentation fournit un moyen propre et fiable d'appeler toute fonctionnalité de nettoyage nécessaire à la fin normale du programme. Évidemment, c'est à foo.cleanup de décider quoi faire avec l'objet lié au nom self.myhandle , mais vous voyez l'idée.

Emballer

Python est un langage puissant et flexible avec de nombreux mécanismes et paradigmes qui peuvent grandement améliorer la productivité. Comme pour tout outil logiciel ou langage, cependant, avoir une compréhension ou une appréciation limitée de ses capacités peut parfois être plus un obstacle qu'un avantage, laissant quelqu'un dans l'état proverbial de "savoir assez pour être dangereux".

Se familiariser avec les nuances clés de Python, telles que (mais sans s'y limiter) les problèmes de programmation modérément avancés soulevés dans cet article, aidera à optimiser l'utilisation du langage tout en évitant certaines de ses erreurs les plus courantes.

Vous pouvez également consulter notre Guide de l'initié sur les entretiens Python pour des suggestions sur les questions d'entretien qui peuvent aider à identifier les experts Python.

Nous espérons que vous avez trouvé les pointeurs de cet article utiles et nous vous invitons à nous faire part de vos commentaires.