Python Language
Pièges courants
Recherche…
Introduction
Python est un langage destiné à être clair et lisible sans aucune ambiguïté ni comportement inattendu. Malheureusement, ces objectifs ne sont pas réalisables dans tous les cas, et c'est pourquoi Python a quelques cas isolés où il pourrait faire quelque chose de différent de ce que vous attendiez.
Cette section vous montrera quelques problèmes que vous pourriez rencontrer lors de l'écriture de code Python.
Changer la séquence sur laquelle vous parcourez
Une boucle for
une itération sur une séquence, donc la modification de cette séquence dans la boucle peut entraîner des résultats inattendus (en particulier lors de l'ajout ou de la suppression d'éléments):
alist = [0, 1, 2]
for index, value in enumerate(alist):
alist.pop(index)
print(alist)
# Out: [1]
Remarque: list.pop()
est utilisé pour supprimer des éléments de la liste.
Le second élément n'a pas été supprimé car l'itération passe par les index dans l'ordre. La boucle ci-dessus est itérée deux fois, avec les résultats suivants:
# Iteration #1
index = 0
alist = [0, 1, 2]
alist.pop(0) # removes '0'
# Iteration #2
index = 1
alist = [1, 2]
alist.pop(1) # removes '2'
# loop terminates, but alist is not empty:
alist = [1]
Ce problème se pose parce que les indices changent tout en itérant dans la direction de l’indice croissant. Pour éviter ce problème, vous pouvez parcourir la boucle en arrière :
alist = [1,2,3,4,5,6,7]
for index, item in reversed(list(enumerate(alist))):
# delete all even items
if item % 2 == 0:
alist.pop(index)
print(alist)
# Out: [1, 3, 5, 7]
En parcourant la boucle à partir de la fin, à mesure que les éléments sont supprimés (ou ajoutés), cela n'affecte pas les index des éléments figurant plus haut dans la liste. Donc, cet exemple va supprimer correctement tous les éléments qui sont même de alist
.
Un problème similaire se pose lors de l’ insertion ou de l’ajout d’éléments à une liste sur laquelle vous parcourez , ce qui peut entraîner une boucle infinie:
alist = [0, 1, 2]
for index, value in enumerate(alist):
# break to avoid infinite loop:
if index == 20:
break
alist.insert(index, 'a')
print(alist)
# Out (abbreviated): ['a', 'a', ..., 'a', 'a', 0, 1, 2]
Sans la condition de break
, la boucle insérait 'a'
tant que l'ordinateur ne manquait pas de mémoire et que le programme est autorisé à continuer. Dans une situation comme celle-ci, il est généralement préférable de créer une nouvelle liste et d'ajouter des éléments à la nouvelle liste lorsque vous parcourez la liste d'origine.
Lors de l'utilisation d'une boucle for
, vous ne pouvez pas modifier les éléments de liste avec la variable d'espace réservé :
alist = [1,2,3,4]
for item in alist:
if item % 2 == 0:
item = 'even'
print(alist)
# Out: [1,2,3,4]
Dans l'exemple ci-dessus, la modification d'un item
ne change rien à la liste d'origine . Vous devez utiliser l'index de liste ( alist[2]
), et enumerate()
fonctionne bien pour cela:
alist = [1,2,3,4]
for index, item in enumerate(alist):
if item % 2 == 0:
alist[index] = 'even'
print(alist)
# Out: [1, 'even', 3, 'even']
A while
boucle pourrait être un meilleur choix dans certains cas:
Si vous souhaitez supprimer tous les éléments de la liste:
zlist = [0, 1, 2]
while zlist:
print(zlist[0])
zlist.pop(0)
print('After: zlist =', zlist)
# Out: 0
# 1
# 2
# After: zlist = []
Bien que la simple réinitialisation de zlist
produira le même résultat;
zlist = []
L'exemple ci-dessus peut également être combiné avec len()
pour s'arrêter après un certain point ou pour supprimer tous les éléments sauf x
de la liste:
zlist = [0, 1, 2]
x = 1
while len(zlist) > x:
print(zlist[0])
zlist.pop(0)
print('After: zlist =', zlist)
# Out: 0
# 1
# After: zlist = [2]
Ou pour parcourir une liste tout en supprimant des éléments répondant à une certaine condition (dans ce cas, supprimer tous les éléments pairs):
zlist = [1,2,3,4,5]
i = 0
while i < len(zlist):
if zlist[i] % 2 == 0:
zlist.pop(i)
else:
i += 1
print(zlist)
# Out: [1, 3, 5]
Notez que vous n'incrémentez pas i
après avoir supprimé un élément. En supprimant l'élément dans zlist[i]
, l'index de l'élément suivant a diminué de un. En cochant zlist[i]
avec la même valeur pour i
lors de la prochaine itération, vous vérifierez correctement l'élément suivant de la liste. .
Une manière contraire de penser à supprimer des éléments indésirables d'une liste consiste à ajouter les éléments souhaités à une nouvelle liste . L'exemple suivant est une alternative à celui - ci while
en exemple de la boucle:
zlist = [1,2,3,4,5]
z_temp = []
for item in zlist:
if item % 2 != 0:
z_temp.append(item)
zlist = z_temp
print(zlist)
# Out: [1, 3, 5]
Ici, nous canalisons les résultats souhaités dans une nouvelle liste. Nous pouvons alors éventuellement réaffecter la liste temporaire à la variable d'origine.
Avec cette tendance, vous pouvez invoquer l’une des fonctionnalités les plus élégantes et les plus puissantes de Python, la compréhension de liste , qui élimine les listes temporaires et diffère de l’idéologie des mutations de la liste / index déjà abordée.
zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]
Argument par défaut mutable
def foo(li=[]):
li.append(1)
print(li)
foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]
Ce code se comporte comme prévu, mais si on ne passe pas un argument?
foo()
# Out: [1] As expected...
foo()
# Out: [1, 1] Not as expected...
En effet, les arguments par défaut des fonctions et des méthodes sont évalués au moment de la définition plutôt que lors de l'exécution. Nous n'avons donc qu'une seule instance de la liste li
.
La manière de le contourner est d'utiliser uniquement des types immuables pour les arguments par défaut:
def foo(li=None):
if not li:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
foo()
# Out: [1]
Bien que ce soit une amélioration et même if not li
correctement évaluée à False
, de nombreux autres objets le font également, comme les séquences de longueur nulle. Les arguments suivants peuvent entraîner des résultats inattendus:
x = []
foo(li=x)
# Out: [1]
foo(li="")
# Out: [1]
foo(li=0)
# Out: [1]
L'approche idiomatique consiste à vérifier directement l'argument contre l'objet None
:
def foo(li=None):
if li is None:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
Liste de multiplication et références communes
Prenons le cas de la création d'une structure de liste imbriquée en multipliant:
li = [[]] * 3
print(li)
# Out: [[], [], []]
À première vue, nous pensons avoir une liste de trois listes imbriquées différentes. Essayons d'ajouter 1
au premier:
li[0].append(1)
print(li)
# Out: [[1], [1], [1]]
1
mais j'ai ajoutées à toutes les listes li
.
La raison en est que [[]] * 3
ne crée pas une list
de 3 list
différentes. Au contraire, il crée une list
contenant 3 références au même objet list
. En tant que tel, lorsque nous ajoutons à li[0]
le changement est visible dans tous les sous-éléments de li
. Ceci est équivalent à:
li = []
element = [[]]
li = element + element + element
print(li)
# Out: [[], [], []]
element.append(1)
print(li)
# Out: [[1], [1], [1]]
Cela peut être corroboré si nous imprimons les adresses mémoire de la list
contenue en utilisant id
:
li = [[]] * 3
print([id(inner_list) for inner_list in li])
# Out: [6830760, 6830760, 6830760]
La solution consiste à créer les listes internes avec une boucle:
li = [[] for _ in range(3)]
Au lieu de créer une list
unique et d'y faire 3 références, nous créons maintenant 3 listes distinctes. Ceci, encore une fois, peut être vérifié en utilisant la fonction id
:
print([id(inner_list) for inner_list in li])
# Out: [6331048, 6331528, 6331488]
Vous pouvez aussi le faire. Il provoque une nouvelle liste vide à créer dans chaque append
appel.
>>> li = []
>>> li.append([])
>>> li.append([])
>>> li.append([])
>>> for k in li: print(id(k))
...
4315469256
4315564552
4315564808
N'utilisez pas d'index pour faire une boucle sur une séquence.
Ne pas:
for i in range(len(tab)):
print(tab[i])
Faire :
for elem in tab:
print(elem)
for
va automatiser la plupart des opérations d'itération pour vous.
Utilisez énumérer si vous avez vraiment besoin de l'index et de l'élément .
for i, elem in enumerate(tab):
print((i, elem))
Soyez prudent lorsque vous utilisez "==" pour vérifier si vrai ou faux
if (var == True):
# this will execute if var is True or 1, 1.0, 1L
if (var != True):
# this will execute if var is neither True nor 1
if (var == False):
# this will execute if var is False or 0 (or 0.0, 0L, 0j)
if (var == None):
# only execute if var is None
if var:
# execute if var is a non-empty string/list/dictionary/tuple, non-0, etc
if not var:
# execute if var is "", {}, [], (), 0, None, etc.
if var is True:
# only execute if var is boolean True, not 1
if var is False:
# only execute if var is boolean False, not 0
if var is None:
# same as var == None
Ne vérifiez pas si vous le pouvez, faites-le simplement et gérez l'erreur
Les pythonistes disent généralement "C'est plus facile de demander le pardon que la permission".
Ne pas:
if os.path.isfile(file_path):
file = open(file_path)
else:
# do something
Faire:
try:
file = open(file_path)
except OSError as e:
# do something
Ou encore mieux avec Python 2.6+
:
with open(file_path) as file:
C'est beaucoup mieux parce que c'est beaucoup plus générique. Vous pouvez appliquer try/except
à presque tout. Vous n'avez pas besoin de vous soucier de ce qu'il faut faire pour le prévenir, prenez simplement soin de l'erreur que vous courez.
Ne pas vérifier avec le type
Python est typé dynamiquement, par conséquent, la vérification de type vous fait perdre de la flexibilité. Au lieu de cela, utilisez le typage de canard en vérifiant le comportement. Si vous attendez une chaîne dans une fonction, utilisez str()
pour convertir n'importe quel objet en chaîne. Si vous vous attendez à une liste, utilisez list()
pour convertir n'importe quelle liste pouvant être itérée.
Ne pas:
def foo(name):
if isinstance(name, str):
print(name.lower())
def bar(listing):
if isinstance(listing, list):
listing.extend((1, 2, 3))
return ", ".join(listing)
Faire:
def foo(name) :
print(str(name).lower())
def bar(listing) :
l = list(listing)
l.extend((1, 2, 3))
return ", ".join(l)
En utilisant la dernière manière, foo
acceptera n'importe quel objet. bar
acceptera les chaînes, les tuples, les ensembles, les listes et bien plus encore. Pas cher SEC.
Ne mélangez pas les espaces et les tabulations
Utiliser l' objet comme premier parent
C'est délicat, mais il va vous mordre à mesure que votre programme se développe. Il existe des classes anciennes et nouvelles dans Python 2.x
Les anciens sont, eh bien, vieux. Ils manquent de certaines fonctionnalités et peuvent avoir un comportement difficile avec l'héritage. Pour être utilisable, n'importe laquelle de vos classes doit être du "nouveau style". Pour ce faire, faites-le hériter de l' object
.
Ne pas:
class Father:
pass
class Child(Father):
pass
Faire:
class Father(object):
pass
class Child(Father):
pass
Dans Python 3.x
toutes les classes sont de nouveau style, vous n'avez donc pas besoin de le faire.
Ne pas initialiser les attributs de classe en dehors de la méthode init
Les personnes venant d'autres langues le trouvent tentant car c'est ce que vous faites en Java ou en PHP. Vous écrivez le nom de la classe, puis listez vos attributs et donnez-leur une valeur par défaut. Il semble fonctionner en Python, cependant, cela ne fonctionne pas comme vous le pensez. Si vous configurez les attributs de classe (attributs statiques), vous obtiendrez sa valeur à moins que ce ne soit vide. Dans ce cas, il retournera les attributs de classe. Cela implique deux grands risques:
Si l'attribut de classe est modifié, la valeur initiale est modifiée.
Si vous définissez un objet mutable comme valeur par défaut, vous obtiendrez le même objet partagé entre les instances.
Ne (sauf si vous voulez statique):
class Car(object):
color = "red"
wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Faire :
class Car(object):
def __init__(self):
self.color = "red"
self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Entier et identité de chaîne
Python utilise la mise en cache interne pour une série d'entiers afin de réduire les surcharges inutiles résultant de leur création répétée.
En effet, cela peut conduire à un comportement déroutant lors de la comparaison d'identités entières:
>>> -8 is (-7 - 1)
False
>>> -3 is (-2 - 1)
True
et, en utilisant un autre exemple:
>>> (255 + 1) is (255 + 1)
True
>>> (256 + 1) is (256 + 1)
False
Attends quoi?
Nous pouvons voir que l'opération d'identité is
le rendement True
pour certains entiers ( -3
, 256
) , mais pas pour d' autres ( -8
, 257
).
Pour être plus précis, les entiers compris dans l'intervalle [-5, 256]
sont mis en cache en interne lors du démarrage de l'interpréteur et ne sont créés qu'une seule fois. En tant que tels, ils sont identiques et la comparaison de leurs identités avec des rendements is
True
; Les entiers en dehors de cette plage sont (généralement) créés à la volée et leurs identités se comparent à False
.
C'est un piège courant, car il s'agit d'un test commun, mais assez souvent, le code échoue dans le processus intermédiaire ultérieur (ou pire - la production) sans raison apparente après avoir parfaitement fonctionné en développement.
La solution consiste à toujours comparer les valeurs en utilisant l' opérateur d' égalité ( ==
) et non l'opérateur ( is
) d'identité.
Python conserve également des références à des chaînes couramment utilisées et peut entraîner un comportement confus similaire lors de la comparaison d'identités (c.-à-d. Utilisant is
) de chaînes.
>>> 'python' is 'py' + 'thon'
True
La chaîne 'python'
est couramment utilisée, donc Python a un objet que toutes les références à la chaîne 'python'
utilisent.
Pour les chaînes peu communes, la comparaison d'identité échoue même lorsque les chaînes sont égales.
>>> 'this is not a common string' is 'this is not' + ' a common string'
False
>>> 'this is not a common string' == 'this is not' + ' a common string'
True
Ainsi, tout comme la règle pour les entiers, comparez toujours les valeurs de chaîne en utilisant l' opérateur d' égalité ( ==
) et non l'opérateur d'identité ( is
).
Accéder aux attributs des littéraux int
Vous avez peut-être entendu dire que tout en Python est un objet, même littéral. Cela signifie, par exemple, que 7
est également un objet, ce qui signifie qu'il possède des attributs. Par exemple, l'un de ces attributs est la bit_length
. Il renvoie la quantité de bits nécessaire pour représenter la valeur sur laquelle il est appelé.
x = 7
x.bit_length()
# Out: 3
En voyant le code ci-dessus fonctionner, vous pourriez penser intuitivement que 7.bit_length()
fonctionnerait aussi bien, seulement pour découvrir qu'il soulève une SyntaxError
. Pourquoi? car l'interpréteur doit différencier un accès d'attribut et un nombre flottant (par exemple 7.2
ou 7.bit_length()
). Ce n'est pas possible, et c'est pourquoi une exception est soulevée.
Il y a plusieurs façons d'accéder aux attributs d'un littéral int
:
# parenthesis
(7).bit_length()
# a space
7 .bit_length()
L'utilisation de deux points (comme ce 7..bit_length()
) ne fonctionne pas dans ce cas, car cela crée un littéral float
et les flottants n'ont pas la méthode bit_length()
.
Ce problème n’existe pas lorsqu’on accède aux attributs des littéraux float
car l’interperter est suffisamment «intelligent» pour savoir qu’un littéral float
ne peut pas en contenir deux .
, par exemple:
7.2.as_integer_ratio()
# Out: (8106479329266893, 1125899906842624)
Chaînage ou opérateur
Lors du test de plusieurs comparaisons d'égalité:
if a == 3 or b == 3 or c == 3:
il est tentant d'abréger cela pour
if a or b or c == 3: # Wrong
C'est faux; le or
a l' opérateur priorité inférieure à ==
, de sorte que l'expression est évaluée comme if (a) or (b) or (c == 3):
. La manière correcte est de vérifier explicitement toutes les conditions:
if a == 3 or b == 3 or c == 3: # Right Way
Alternativement, la fonction any()
intégrée peut être utilisée à la place des chaînes or
opérateurs:
if any([a == 3, b == 3, c == 3]): # Right
Ou, pour le rendre plus efficace:
if any(x == 3 for x in (a, b, c)): # Right
Ou, pour le raccourcir:
if 3 in (a, b, c): # Right
Ici, nous utilisons l'opérateur in
pour tester si la valeur est présente dans un tuple contenant les valeurs que nous voulons comparer.
De même, il est incorrect d'écrire
if a == 1 or 2 or 3:
qui devrait être écrit comme
if a in (1, 2, 3):
sys.argv [0] est le nom du fichier en cours d'exécution
Le premier élément de sys.argv[0]
est le nom du fichier python en cours d'exécution. Les éléments restants sont les arguments du script.
# script.py
import sys
print(sys.argv[0])
print(sys.argv)
$ python script.py
=> script.py
=> ['script.py']
$ python script.py fizz
=> script.py
=> ['script.py', 'fizz']
$ python script.py fizz buzz
=> script.py
=> ['script.py', 'fizz', 'buzz']
Les dictionnaires ne sont pas ordonnés
Vous pouvez vous attendre à ce qu'un dictionnaire Python soit trié par des clés comme, par exemple, un std::map
C ++, mais ce n'est pas le cas:
myDict = {'first': 1, 'second': 2, 'third': 3}
print(myDict)
# Out: {'first': 1, 'second': 2, 'third': 3}
print([k for k in myDict])
# Out: ['second', 'third', 'first']
Python n'a pas de classe intégrée qui trie automatiquement ses éléments par clé.
Toutefois, si le tri n'est pas obligatoire et que vous souhaitez simplement que votre dictionnaire retienne l'ordre d'insertion de ses paires clé / valeur, vous pouvez utiliser des collections.OrderedDict
:
from collections import OrderedDict
oDict = OrderedDict([('first', 1), ('second', 2), ('third', 3)])
print([k for k in oDict])
# Out: ['first', 'second', 'third']
Gardez à l'esprit que l'initialisation d'un OrderedDict
avec un dictionnaire standard ne vous permettra en aucun cas de trier le dictionnaire. Tout ce que fait cette structure est de préserver l'ordre d'insertion des clés.
L'implémentation des dictionnaires a été modifiée dans Python 3.6 pour améliorer leur consommation de mémoire. Un effet secondaire de cette nouvelle implémentation est qu’elle préserve également l’ordre des arguments de mots clés transmis à une fonction:
def func(**kw): print(kw.keys())
func(a=1, b=2, c=3, d=4, e=5)
dict_keys(['a', 'b', 'c', 'd', 'e']) # expected order
Mise en garde: méfiez-vous que « l'aspect préservation de la commande de cette nouvelle implémentation est considéré comme un détail d'implémentation et il ne faut pas s'y fier » , car cela pourrait changer à l'avenir.
Global Interpreter Lock (GIL) et les threads de blocage
Beaucoup de choses ont été écrites sur le GIL de Python . Cela peut parfois causer de la confusion lorsque vous traitez des applications multithread (à ne pas confondre avec les multiprocessus).
Voici un exemple:
import math
from threading import Thread
def calc_fact(num):
math.factorial(num)
num = 600000
t = Thread(target=calc_fact, daemon=True, args=[num])
print("About to calculate: {}!".format(num))
t.start()
print("Calculating...")
t.join()
print("Calculated")
Vous vous attendriez à voir Calculating...
imprimé immédiatement après le démarrage du thread, après tout, nous voulions que le calcul ait lieu dans un nouveau thread! Mais en réalité, vous le voyez imprimé une fois le calcul terminé. C'est parce que le nouveau thread s'appuie sur une fonction C ( math.factorial
) qui verrouille le GIL lors de son exécution.
Il y a plusieurs manières de contourner cela. La première consiste à implémenter votre fonction factorielle en Python natif. Cela permettra au thread principal de prendre le contrôle pendant que vous êtes dans votre boucle. L'inconvénient est que cette solution sera beaucoup plus lente, car nous n'utilisons plus la fonction C.
def calc_fact(num):
""" A slow version of factorial in native Python """
res = 1
while num >= 1:
res = res * num
num -= 1
return res
Vous pouvez également sleep
pendant un certain temps avant de commencer votre exécution. Remarque: cela ne permettra pas à votre programme d’interrompre le calcul qui se produit à l’intérieur de la fonction C, mais cela permettra à votre thread principal de continuer après l’apparition, ce à quoi vous pouvez vous attendre.
def calc_fact(num):
sleep(0.001)
math.factorial(num)
Variable de fuite dans les listes compréhensibles et pour les boucles
Considérer la compréhension de liste suivante
i = 0
a = [i for i in range(3)]
print(i) # Outputs 2
Cela se produit uniquement dans Python 2 en raison du fait que la compréhension de la liste «fuit» la variable de contrôle de la boucle dans la portée environnante ( source ). Ce comportement peut conduire à des bogues difficiles à trouver et a été corrigé dans Python 3 .
i = 0
a = [i for i in range(3)]
print(i) # Outputs 0
De même, pour les boucles n'ont pas de portée privée pour leur variable d'itération
i = 0
for i in range(3):
pass
print(i) # Outputs 2
Ce type de comportement se produit à la fois dans Python 2 et Python 3.
Pour éviter les problèmes avec les variables qui fuient, utilisez les nouvelles variables dans les listes compréhensibles et les boucles, le cas échéant.
Retour multiple
La fonction xyz renvoie deux valeurs a et b:
def xyz():
return a, b
Le code appelant xyz stocke les résultats dans une variable en supposant que xyz ne renvoie qu'une seule valeur:
t = xyz()
La valeur de t
est en fait un tuple (a, b), donc toute action sur t
supposant que ce n'est pas un tuple peut échouer profondément dans le code avec une erreur inattendue sur les tuples.
TypeError: le type tuple ne définit pas la méthode ...
La solution serait de faire:
a, b = xyz()
Les débutants auront du mal à trouver la raison de ce message en lisant uniquement le message d'erreur tuple!
Clés Pythonic JSON
my_var = 'bla';
api_key = 'key';
...lots of code here...
params = {"language": "en", my_var: api_key}
Si vous avez l'habitude de JavaScript, l'évaluation des variables dans les dictionnaires Python ne correspondra pas à vos attentes. Cette instruction en JavaScript entraînerait l'objet params
comme suit:
{
"language": "en",
"my_var": "key"
}
En Python, cependant, le dictionnaire suivant apparaîtrait:
{
"language": "en",
"bla": "key"
}
my_var
est évalué et sa valeur est utilisée comme clé.