Python est un langage de programmation de haut niveau orienté objet et interprété avec une sémantique dynamique. Son haut niveau de structures de données intégrées, combiné à une écriture et une reliure dynamiques le rendent très attrayant pour les développement rapide d'applications , ainsi que pour une utilisation en tant que langage de script ou colle pour connecter des composants ou des services existants. Python fonctionne avec des modules et des packages, favorisant ainsi la modularité du programme et la réutilisation du code.
Syntaxe Python simple et facile à apprendre, vous pouvez envoyer au développeurs python dans la mauvaise direction - en particulier ceux qui apprennent la langue - perdant certaines de ses subtilités en cours de route et sous-estimant le pouvoir de Langage diversifié Python .
Dans cet esprit, cet article présente une liste des «10 meilleurs» bogues subtils et difficiles à voir qui peuvent prendre au dépourvu même certains des développeurs Python les plus avancés.
( Remarque: Cet article est destiné à un public plus avancé que les erreurs de programmation Python courantes, qui s'adressent davantage à ceux qui sont nouveaux dans le langage. )
Python vous permet de spécifier qu'un argument de la fonction est facultatif, en lui fournissant une valeur par défaut. Bien que ce soit 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 la 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 idée fausse courante est que l'argument optionnel sera défini sur l'expression par défaut spécifique, chaque fois que la fonction est appelée sans qu'il soit nécessaire de fournir une valeur pour l'argument optionnel. Dans le code ci-dessus, par exemple, vous pouvez vous attendre à appeler foo()
plusieurs fois (c'est-à-dire sans spécifier d'argument de barre) renverrait toujours baz
, puisque l'hypothèse serait que chaque fois foo()
est appelée (sans argument de barre spécifié) bar est défini sur []
(c'est-à-dire une nouvelle liste vide).
Mais voyons ce qui se passe réellement lorsque cela est fait:
>>> foo() ['baz'] >>> foo() ['baz', 'baz'] >>> foo() ['baz', 'baz', 'baz']
Hey? Pourquoi la valeur par défaut de baz
à une liste existante chaque fois que foo()
a été appelé, au lieu de créer une nouvelle liste à chaque occasion? 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. Par conséquent, l'argument bar est initialisé à sa valeur par défaut (c'est-à-dire une liste vide) uniquement lorsque foo()
a été défini en premier, mais appelle ensuite foo()
(autrement dit, sans un argument bar
spécifié), ils utiliseront toujours la même liste que bar
a été initialisé à l'origine.
À propos, une solution 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']
Prenons l'exemple suivant:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1
Logique.
>>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1
Oui, encore comme prévu.
>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3
Qu'est ce que c'est? On change seulement A.x
Pourquoi C.x
changé aussi?
En Python, les variables de classe sont gérées en interne comme des dictionnaires et suivent ce que l'on appelle souvent Ordre de résolution de méthode (MRO) . Donc 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 (uniquement A dans l'exemple ci-dessus, bien que Python supporte l'héritage multiple). En d'autres termes, C n'a pas sa propre propriété x, indépendante de A. Par conséquent, les références à C.x sont en fait des références à A.x. Cela provoque un problème Python, sauf s'il est géré correctement. Plus d'informations sur attributs de classe en Python .
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 '', line 3, in IndexError: list index out of range
Le problème ici est que le rapport except
il ne prend pas une liste d'exceptions spécifiées de cette manière. En revanche, Python 2.x la syntaxe except Exception, e
, est utilisée pour lier l'exception au deuxième paramètre optionnel spécifié (dans ce cas e
), afin de le rendre disponible pour une inspection plus approfondie. Par conséquent, dans le code ci-dessus, l'exception IndexError
n'est pas capturé par le rapport except
; au contraire, l'exception finit par être liée à un paramètre appelé IndexError
.
La manière correcte pour intercepter plusieurs exceptions dans un rapport except
, est de spécifier le premier paramètre comme un double Un contenant toutes les exceptions à intercepter. De plus, pour une portabilité maximale, utilisez le mot-clé as
, car la syntaxe est prise en charge par Python 2 et Python 3:
>>> try: ... l = ['a', 'b'] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
La résolution de la portée de Python est basée sur ce que l'on appelle la règle LEGB , qui est l'abréviation de L ocal, EST nclosing, g lobal, B uilt-in. Cela semble assez simple, non? 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 commun et plus avancé ci-dessous.
Considérer ce qui suit:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File '', line 1, in File '', line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
Quel est le problème?
L'erreur ci-dessus est due au fait que lorsqu'un affectation à une variable dans une portée, cette variable est considérée, automatiquement par Python, comme locale dans cette portée et suit toute variable de nom similaire, dans n'importe quelle portée externe.
Beaucoup d'entre eux sont donc surpris d'obtenir un UnboundLocalError
dans le code de travail précédent, lorsqu'il est modifié en ajoutant une instruction de rapport, quelque part dans le corps d'une fonction. (Vous pouvez en savoir plus à ce sujet ici .)
Il est particulièrement courant que cela déroute les développeurs lors de l'utilisation listes . Prenons 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 '', line 1, in File '', line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Hey? Pourquoi foo2
échoué, tandis que foo1
a très bien fonctionné?
La réponse est la même que le problème de l'exemple précédent, mais elle est certainement plus subtile. foo1
Ne fait pas un affectation a lst
, tandis que foo2
Si c'est. En se souvenant que lst += [5]
est en fait l'abréviation de lst = lst + [5]
, nous voyons que nous essayons d'attribuer une valeur à lst
(par conséquent, Python suppose que vous êtes dans la portée locale). Cependant, la valeur que nous essayons d'attribuer à lst est basée sur le même lst
(encore une fois présumée locale), qui reste à définir. Boom.
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 '', line 2, in IndexError: list index out of range
La suppression d'un élément d'une liste ou d'un tableau, tout en l'itérant, est un problème Python bien connu de tout développeur de logiciel expérimenté. Cependant, bien que l'exemple ci-dessus puisse être assez évident, même les développeurs avancés peuvent par inadvertance être pris au dépourvu par ce 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 entraîner un code considérablement simplifié et rationalisé. Un avantage secondaire de ceci est que le code plus simple est moins susceptible d'être attrapé par le bogue de suppression accidentelle d'un élément de liste lors de son itération. L'un de ces paradigmes est [list comprehensions] ((https://docs.python.org/2/tutorial/datastructures.html#tut-listcomps). D'autre part, les list comprehensions sont particulièrement utiles pour éviter ce problème spécifique, car 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)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
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 devez vous attendre au résultat suivant:
0 2 4 6 8
Mais vous obtenez en fait:
8 8 8 8 8
Surprise!
Cela se produit en raison du comportement lien tardif Python, qui dit que les valeurs des variables utilisées dans les fermetures sont récupérées au moment où la fonction interne est appelée. Ainsi, dans le code ci-dessus, lorsque l'une des fonctions renvoyées est appelée, la valeur de i
voulu dans la zone qui l'entoure au moment où on l'appelle (et à ce stade, le cercle est terminé, donc i
a déjà reçu sa valeur finale de 4).
La solution à ce problème Python courant 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
Voilà! Nous profitons des arguments par défaut pour générer des fonctions anonymes, afin d'obtenir le comportement souhaité. Certains appelleraient cela, élégant. Certains appelleraient cela subtile. Certains détestent ça. Mais si vous êtes un développeur Python, il est important de le comprendre.
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 b.x print f()
Et dans b.py
:
import a x = 1 def g(): print a.f()
Essayons d'abord d'importer a.py
:
>>> import a 1
Cela a très bien fonctionné. Peut-être avez-vous été surpris. Après tout, nous avons ici une importation circulaire qui devrait vraisemblablement être un problème, non?
La réponse est que le simple présence d'un import circulaire n'est pas en tant que tel un problème en Python. Si un module a déjà été importé, Python est suffisamment intelligent pour ne pas essayer de l'importer à nouveau. Cependant, selon le point auquel chaque module tente d'accéder aux fonctions ou aux variables définies dans l'autre, vous pouvez rencontrer des problèmes.
Revenons donc à notre exemple, lorsque nous avons importé a.py
, je n'ai eu aucun problème à importer b.py
, puisque b.py
ne nécessite aucun b.py
à définir au moment de l'importation. La seule référence dans b.py
a a
, est l'appel à a.f()
. Mais cet appel est dans g()
et rien dans a.py
ou b.py
invoque g()
. Alors, la vie est belle.
Mais que se passe-t-il si vous essayez d'importer b.py
(bien sûr, sans avoir préalablement importé a.py
):
>>> import b Traceback (most recent call last): File '', line 1, in File 'b.py', line 1, in import a File 'a.py', line 6, in print f() File 'a.py', line 4, in f return b.x AttributeError: 'module' object has no attribute 'x'
Oh oh. Ce n'est pas bon! Le problème ici est que dans le processus d'importation b.py
il essaie d'importer a.py
, qui en conséquence appelle f()
, qui à son tour essaie d'accéder à b.x
. Mais b.x
il n'a pas encore été défini. D'où l'exception AttributeError
.
Au moins une solution à cela est assez triviale. Modifiez simplement b.py
importer a.py
à l'intérieur g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print a.f()
Une fois importé, tout va bien:
>>> import b >>> b.g() 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'
L'un des avantages de Python est le grand nombre de modules de bibliothèque fournis depuis le début. Mais par conséquent, si vous n'évitez pas consciemment cela, il n'est pas si difficile de rencontrer un conflit de nom, entre le nom de l'un de vos modules et un module du même nom dans la bibliothèque standard livrée avec Python (pour exemple, vous pouvez avoir un module nommé email.py
dans votre code, qui entrerait en conflit avec le module de bibliothèque standard du même nom).
Cela peut conduire à des problèmes très agressifs, comme l'importation d'une autre bibliothèque qui à son tour essaie d'importer la version des bibliothèques standard Python d'un module, mais comme vous avez déjà un module avec le même nom, l'autre package importe par erreur votre version, à la place de celui trouvé dans la bibliothèque standard Python, et c'est là que les erreurs les plus graves se produisent.
Par conséquent, il faut veiller à éviter d'utiliser les mêmes noms que les modules de bibliothèque Python standard. Il vous est beaucoup plus facile de renommer un module dans votre package que de présenter une proposition d'amélioration Python. (DYNAMISME) pour demander un changement de nom en amont et le faire approuver.
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()
En Python 2, cela fonctionne très bien:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Mais maintenant, faisons un tour sur Python 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File 'foo.py', line 19, in bad() File 'foo.py', line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
Qu'est-ce qui vient de se passer ici? Le 'problème' est que dans Python 3, l'objet d'exception n'est pas accessible au-delà de la portée du bloc except
. (La raison en est qu'il conserverait autrement une boucle 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 de contourner ce problème consiste à conserver une référence à l'objet d'exception hors de la portée du bloc except afin qu'il reste accessible. Voici une version de l'exemple ci-dessus qui utilise cette technique, donc dosant le code et le rendant plus 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()
L'exécution de ceci est dans Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
¡ Yupi!
(Au fait, notre Guide de recrutement Python présente un certain nombre de différences importantes, à prendre en compte lors de la migration de votre code de Python 2 vers Python 3.)
__del__
Disons que vous aviez ceci dans un fichier appelé mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
Et puis vous avez essayé de faire cela depuis another_mod.py
:
import mod mybar = mod.Bar()
Vous obtiendrez une vilaine exception AttributeError
.
Parce que? Parce que, comme indiqué ici Lorsque l'interpréteur est désactivé, les variables globales du module sont définies sur None
. En conséquence, dans l'exemple ci-dessus, au point où __del__
invoqué, le nom foo a déjà été défini sur None
.
Une solution à ce problème un peu plus avancée que la programmation Python, serait d'utiliser atexit.register()
à sa place. De cette façon, lorsque le programme est exécuté (sortant normalement, je veux dire), vos gestionnaires enregistrés sont expulsés. avant que l'interprète éteint.
Avec cette connaissance, une solution pour le code précédent mod.py
ça pourrait être quelque chose comme ça:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Cette application offre un moyen propre et fiable d'appeler toute fonctionnalité de nettoyage nécessaire après la fin normale du programme. Évidemment, c'est à foo.cleanup de décider quoi faire de l'objet attaché au nom self.myhandle, mais vous comprenez.
Python est un langage puissant et flexible avec de nombreux mécanismes et paradigmes qui peuvent grandement améliorer la productivité. Cependant, comme pour tout logiciel ou outil de langage, avoir une compréhension ou une appréciation limitée de ses capacités peut parfois être plus un obstacle qu'un atout, nous laissant 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 abordés dans cet article, vous aidera à optimiser votre utilisation du langage, en évitant certaines de ses erreurs les plus courantes.
Vous devriez consulter notre Guide d'initiés sur les entretiens Python , pour des suggestions sur les questions d'entrevue qui peuvent aider à identifier les experts Python.
Nous espérons que vous trouverez les conseils de cet article utiles et nous apprécions vos commentaires.