À propos de Python
Python est un langage de programmation interprété, orienté objet et de haut niveau avec une sémantique dynamique. Les structures de données de haut niveau intégrées combinées à la frappe dynamique et à la liaison dynamique le rendent très attrayant pour BRPS (développement rapide d'applications), ainsi que pour une utilisation en tant que langage de script et de connexion 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 simplicité et la facilité d'apprentissage de ce langage peuvent être déroutantes pour les développeurs (en particulier ceux qui commencent tout juste à apprendre Python), vous pouvez donc perdre de vue certaines subtilités importantes et sous-estimer la puissance de la variété des solutions possibles utilisant Python.
Dans cet esprit, cet article présente le «top 10» des erreurs subtiles et difficiles à trouver que même les développeurs Python avancés peuvent commettre.
Erreur # 1: mauvaise utilisation des expressions comme valeurs par défaut pour les arguments de fonction
Python vous permet d'indiquer qu'une fonction peut avoir des arguments facultatifs en définissant une valeur par défaut pour eux. Ceci, bien sûr, est une caractéristique très pratique du langage, mais peut entraîner des conséquences désagréables si le type de cette valeur est modifiable. Par exemple, considérez la définition de fonction suivante:
>>> def foo(bar=[]):
Une erreur courante dans ce cas est de penser que la valeur d'un argument facultatif sera définie sur une valeur par défaut à chaque fois qu'une fonction est appelée sans valeur pour cet argument. Dans le code ci-dessus, par exemple, nous pouvons supposer qu'en appelant à plusieurs reprises la fonction foo () (c'est-à-dire sans spécifier de valeur pour l'argument bar), elle renverra toujours «baz», car il est supposé que chaque fois que foo () est appelé (sans spécifiant l'argument bar), bar est défini sur [] (c'est-à-dire une nouvelle liste vide).
Mais voyons ce qui se passera réellement:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
Hein? Pourquoi la fonction continue-t-elle d'ajouter la valeur par défaut «baz» à la liste existante à chaque appel de foo (), au lieu de créer une nouvelle liste à chaque fois?
La réponse à cette question sera une compréhension plus approfondie de ce qui se passe avec Python «sous le capot». A savoir: la valeur par défaut de la fonction n'est initialisée qu'une seule fois, lors de la définition de la fonction. Ainsi, l'argument bar est initialisé par défaut (c'est-à-dire une liste vide) uniquement lorsque foo () est défini pour la première fois, mais les appels ultérieurs à foo () (c'est-à-dire sans spécifier l'argument bar) continueront à utiliser la même liste qui était créé pour la barre d'argument lors de la première définition de fonction.
Pour référence, une «solution de contournement» courante pour cette erreur est la définition suivante:
>>> def foo(bar=None): ... if bar is None:
Erreur # 2: mauvaise utilisation des variables de classe
Prenons l'exemple suivant:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1
Tout semble être en ordre.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
Ouais, tout était comme prévu.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
Que diable?! Nous venons de changer Ax. Pourquoi Cx a-t-il changé aussi?
En Python, les variables de classe sont traitées comme des dictionnaires et suivent ce qu'on appelle souvent l'ordre de résolution de méthode (MRO). Ainsi, dans le code ci-dessus, puisque l'attribut x ne se trouve pas dans la classe C, il le sera 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. Ainsi, les références à Cx sont en fait des références à Ax. Cela causera des problèmes si ces cas ne sont pas traités correctement. Ainsi, lorsque vous apprenez Python, portez une attention particulière aux attributs de classe et travaillez avec eux.
Erreur n ° 3: paramètres incorrects pour le bloc d'exception
Supposons que vous ayez le code suivant:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError:
Le problème ici est que l'expression d'exception n'accepte pas la liste des exceptions spécifiées de cette manière. Au lieu de cela, dans Python 2.x, l'expression «sauf exception, e» est utilisée pour lier l'exception à un second paramètre facultatif donné (dans ce cas, e) pour le rendre disponible pour une inspection plus approfondie. Par conséquent, dans le code ci-dessus, une exception IndexError n'est pas interceptée par l'instruction except; à la place, l'exception se termine par la liaison à un paramètre nommé IndexError.
La façon correcte d'attraper plusieurs exceptions avec l'expression d'exception consiste à spécifier le premier paramètre sous la forme d'un tuple contenant toutes les exceptions que vous souhaitez intercepter. De plus, pour une compatibilité maximale, utilisez le mot clé as, car cette syntaxe est prise en charge à la fois dans Python 2 et Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Erreur # 4: incompréhension des règles de portée Python
La portée en Python est basée sur la soi-disant règle LEGB, qui est une abréviation de Local (noms attribués de quelque manière que ce soit à l'intérieur d'une fonction (def ou lambda), et non déclarée globale dans cette fonction), Enclosing (nom dans la portée locale de toute fonction incluant statiquement ( def ou lambda), de interne à externe), Global (noms attribués au niveau supérieur du fichier de module, ou en exécutant les instructions globales en def dans le fichier), Built-in (noms précédemment attribués dans le module de nom intégré: open, range, SyntaxError, ...). Cela semble assez simple, non? Eh bien, en fait, il existe quelques subtilités sur la façon dont cela fonctionne en Python, ce qui nous amène au problème de programmation Python plus complexe ci-dessous. Prenons 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 se produit car lorsque vous affectez une variable dans la portée, Python la considère automatiquement comme locale pour cette portée et masque toute variable portant le même nom dans n'importe quelle portée parent.
Ainsi, beaucoup sont surpris lorsqu'ils reçoivent UnboundLocalError dans du code en cours d'exécution, lorsqu'il est modifié en ajoutant un opérateur d'affectation quelque part dans le corps de la fonction.
Cette fonctionnalité est particulièrement déroutante pour les développeurs lors de l'utilisation de listes. Prenons l'exemple suivant:
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5)
Hein? Pourquoi foo2 plante-t-il alors que foo1 fonctionne correctement?
La réponse est la même que dans l'exemple précédent, mais, selon la croyance populaire, la situation ici est plus subtile. foo1 n'applique pas l'opérateur d'affectation à lst, contrairement à foo2. En gardant à l'esprit que lst + = [5] n'est en fait qu'un raccourci pour lst = lst + [5], nous voyons que nous essayons d'affecter la valeur lst (donc Python suppose qu'elle est de portée locale). Cependant, la valeur que nous voulons attribuer à lst est basée sur lst lui-même (encore une fois, il est maintenant supposé être de portée locale), qui n'a pas encore été déterminée. Et nous obtenons une erreur.
Erreur # 5: changer une liste pendant l'itération dessus
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]
La suppression d'un élément d'une liste ou d'un tableau pendant l'itération est un problème Python bien connu de tout développeur de logiciels expérimenté. Mais, bien que l'exemple ci-dessus puisse être assez évident, même les développeurs expérimentés peuvent se lancer dans ce râteau dans un code beaucoup plus complexe.
Heureusement, Python comprend un certain nombre de paradigmes de programmation élégants qui, lorsqu'ils sont utilisés correctement, peuvent conduire à une simplification et une optimisation significatives du code. Une autre conséquence agréable de ceci est que dans un code plus simple, la probabilité de tomber dans l'erreur de supprimer accidentellement un élément de liste pendant l'itération sur lui est beaucoup moins. Un tel paradigme est celui des générateurs de listes. De plus, la compréhension du fonctionnement des générateurs de listes est particulièrement utile pour éviter ce problème particulier, comme le montre cette implémentation alternative du code ci-dessus, qui fonctionne très bien:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)]
Erreur # 6: malentendu sur la façon dont Python lie les variables dans les fermetures
Prenons 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 à la sortie suivante:
0 2 4 6 8
Mais en réalité, vous obtenez ceci:
8 8 8 8 8
Surprise!
Cela est dû à une liaison tardive en Python, ce qui signifie que les valeurs des variables utilisées dans les fermetures sont recherchées lors de l'appel à la fonction interne. Ainsi, dans le code ci-dessus, chaque fois que l'une des fonctions retournées est appelée, la valeur de i est recherchée dans la portée environnante lors de son appel (et à ce moment-là, le cycle était déjà terminé, par conséquent, le résultat final avait déjà été attribué - valeur 4) .
La solution à ce problème Python courant 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
Voila! Nous utilisons ici les arguments par défaut pour générer des fonctions anonymes afin d'obtenir le comportement souhaité. Certains qualifieraient cette solution d'élégante. Certains sont
mince. Certaines personnes détestent ces choses. Mais si vous êtes un développeur Python, de toute façon, il est important de comprendre.
Erreur # 7: création de dépendances de module cycliques
Supposons que vous ayez deux fichiers, a.py et b.py, chacun important l'autre, comme suit:
Dans a.py:
import b def f(): return bx print f()
Dans b.py:
import a x = 1 def g(): print af()
Tout d'abord, essayez d'importer a.py:
>>> import a 1
Cela a très bien fonctionné. Cela peut vous surprendre. Après tout, les modules s’importent cycliquement et cela devrait probablement être un problème, non?
La réponse est que la simple importation cyclique de modules n'est pas en soi un problème en Python. Si le module a déjà été importé, Python est suffisamment intelligent pour ne pas essayer de le réimporter. Cependant, selon le point auquel chaque module essaie d'accéder aux fonctions ou variables définies dans un autre, vous pouvez réellement rencontrer des problèmes.
Donc, pour revenir à notre exemple, lorsque nous avons importé a.py, il n'a eu aucun problème à importer b.py, car b.py ne nécessite pas de définir a.py lors de son importation. La seule référence dans b.py à a est un appel à af (). Mais cet appel dans g () et rien dans a.py ou b.py n'appelle pas g (). Donc, tout fonctionne bien.
Mais que se passe-t-il si nous essayons d'importer b.py (sans d'abord importer 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 pendant le processus d'importation de b.py, il essaie d'importer a.py, qui à son tour appelle f (), qui essaie d'accéder à bx Mais bx n'a pas encore été défini. D'où l'exception AttributeError.
Au moins une solution à ce problème est assez banale. Modifiez simplement b.py pour importer a.py dans g ():
x = 1 def g(): import a
Maintenant, quand nous l'importons, tout va bien:
>>> import b >>> bg() 1
Erreur # 8: intersection de noms avec des noms de modules dans la bibliothèque standard Python
L'un des charmes de Python est ses nombreux modules qui sortent de la boîte. Mais par conséquent, si vous ne suivez pas consciemment cela, vous pouvez constater que le nom de votre module peut avoir le même nom que le module dans la bibliothèque standard fournie avec Python (par exemple, dans votre code, il peut y avoir un module avec le nom email.py, qui entrera en conflit avec le module de bibliothèque standard du même nom).
Cela peut entraîner de graves problèmes. Par exemple, si l'un des modules essaie d'importer la version du module à partir de la bibliothèque standard Python et que vous avez un module dans le projet avec le même nom, qui sera importé par erreur au lieu du module de la bibliothèque standard.
Par conséquent, il convient de ne pas utiliser les mêmes noms que dans les modules de la bibliothèque standard Python. Il est beaucoup plus facile de changer le nom du module dans votre projet que de soumettre une demande pour changer le nom du module dans la bibliothèque standard et obtenir son approbation.
Erreur n ° 9: non prise en compte des différences entre Python 2 et Python 3
Considérez le fichier foo.py suivant:
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 fonctionnera bien:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Mais maintenant, voyons comment cela fonctionnera en 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
Qu'est-ce qui vient de se passer ici? Le "problème" est qu'en Python 3, un objet dans un bloc d'exception n'est pas disponible en dehors de celui-ci. (La raison en est qu'autrement, les objets de ce bloc seront stockés en mémoire jusqu'à ce que le garbage collector démarre et en supprime les références).
Une façon d'éviter ce problème consiste à conserver la référence à l'objet bloc d'exception en dehors de ce bloc afin qu'il reste disponible. Voici la version de l'exemple précédent qui utilise cette technique, obtenant ainsi du code adapté à la fois à 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écutez-le dans Python 3:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
Hourra!
Erreur # 10: mauvaise utilisation de la méthode __del__
Disons que vous avez un fichier mod.py comme celui-ci:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
Et vous essayez de le faire à partir d'un autre another_mod.py:
import mod mybar = mod.Bar()
Et obtenez une terrible AttributeError.
Pourquoi? Parce que, comme indiqué
ici , lorsque l'interpréteur s'arrête, les variables globales du module ont toutes la valeur None. Par conséquent, dans l'exemple ci-dessus, lorsque __del__ a été appelé, le nom foo était déjà défini sur Aucun.
La solution à cette "tâche avec un astérisque" consiste à utiliser atexit.register (). Ainsi, lorsque votre programme termine l'exécution (c'est-à-dire lorsqu'il se termine normalement), vos descripteurs sont supprimés avant que l'interpréteur ne termine son travail.
Dans cet esprit, le correctif pour le code mod.py ci-dessus pourrait ressembler à ceci:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Une telle implémentation fournit un moyen simple et fiable d'appeler tout nettoyage nécessaire après la fin d'un programme normal. Évidemment, la décision sur la façon de traiter l'objet associé au nom self.myhandle est laissée à foo.cleanup, mais je pense que vous comprenez l'idée.
Conclusion
Python est un langage puissant et flexible avec de nombreux mécanismes et paradigmes qui peuvent améliorer considérablement les performances. Cependant, comme avec tout outil logiciel ou langage, avec une compréhension ou une évaluation limitée de ses capacités, des problèmes imprévus peuvent survenir pendant le développement.
Une introduction aux nuances Python abordées dans cet article vous aidera à optimiser votre utilisation du langage, tout en évitant certaines erreurs courantes.