Les listes et tuples
Aussi disponible en mode diaporama…
- Info
Aller plus loin avec les tuples
On a vu que la virgule était le constructeur du tuple, pas les parenthèses. Par exemple :
>>> 1,
(1,)
L'interpréteur Python montre les parenthèses pour que ce soit plus clair et je vous conseille de faire de même :
>>> (1,)
(1,)
Mais n'oubliez pas la virgule !
>>> (1)
1
Dans un tuple contenant un seul élément, la virgule est nécessaire. Dans un tuple avec plus de 2 éléments, la virgule finale est optionnelle. Pour un tuple vide, une paire de parenthèses suffit :
>>> ()
()
>>> tuple()
()
Une erreur de typo courante est de laisser une virgule alors que vous ne souhaitez pas avoir un tuple. Il est très facile de l'oublier dans votre code :
>>> value = 1,
>>> value
(1,)
Donc si vous vous retrouvez avec un tuple alors que vous ne vous y attendiez pas, cherchez la virgule ! (Note du traducteur : de ma propre expérience, il est plus courant d'oublier la virgule pour un tuple ne contenant qu'un seul élément, dans les settings de Django par exemple, cherchez plutôt la virgule manquante dans ces cas là).
Index & Item (1)
Voici une manière élégante de vous épargner quelques lignes si vous avez besoin d'une liste de mots :
>>> items = 'zero one two three'.split()
>>> print items
['zero', 'one', 'two', 'three']
Prenons l'exemple d'un itération entre les items d'une liste, pour laquelle nous voulons à la fois l'item et la position (l'index) de cet item dans la liste :
- ou -
i = 0
for item in items: for i in range(len(items)):
print i, item print i, items[i]
i += 1
Index & Item (2): enumerate
La fonction enumerate prend une liste et retourne des paires (index, item) :
>>> print list(enumerate(items))
[(0, 'zero'), (1, 'one'), (2, 'two'), (3, 'three')]
Il est nécessaire d'avoir recours à une list pour afficher les résultats
car enumerate est une fonction fainéante, générant un item (une paire) à
la fois, seulement lorsqu'il est demandé. Une boucle for nécessite un tel
mécanisme. enumerate est un exemple de générateur dont on parlera plus
tard des détails. print ne prend pas un résultat à la fois mais doit être
en possession de la totalité du message à afficher. On a donc converti
automatiquement le générateur en une liste avant d'utiliser print.
Notre boucle devient beaucoup plus simple :
for (index, item) in enumerate(items):
print index, item
# comparé à : # comparé à :
index = 0 for i in range(len(items)):
for item in items: print i, items[i]
print index, item
index += 1
La version avec enumerate est plus courte et plus simple que la version
de gauche, et plus facile à lire que les deux autres.
Un exemple montrant que la fonction enumerate retourne un itérateur
(un générateur est une sorte d'itérateur) :
>>> enumerate(items)
<enumerate object at 0x011EA1C0>
>>> e = enumerate(items)
>>> e.next()
(0, 'zero')
>>> e.next()
(1, 'one')
>>> e.next()
(2, 'two')
>>> e.next()
(3, 'three')
>>> e.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
StopIteration
List Comprehensions
Les list comprehensions ("listcomps" pour les intimes) sont des raccourcis syntaxiques pour ce pattern généralement utilisé.
La manière traditionnelle avec for et if :
new_list = []
for item in a_list:
if condition(item):
new_list.append(fn(item))
En utilisant une list comprehension :
new_list = [fn(item) for item in a_list
if condition(item)]
Les listcomps sont claires et concises, directes. Vous pouvez avoir plusieurs
boucles for et conditions if au sein d'une même listcomp, mais
au-delà de deux ou trois, ou si les conditions sont complexes, je vous
suggère d'utiliser l'habituelle boucle for. En appliquant le Zen de Python,
utilisez la méthode la plus lisible.
Par exemple, la liste des carrés de 0 à 9 :
>>> [n ** 2 for n in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
La liste des nombres premiers au sein de la précédente liste :
>>> [n ** 2 for n in range(10) if n % 2]
[1, 9, 25, 49, 81]
Generator Expressions (1)
Faisons la somme des carrés des nombres inférieurs à 100 :
Avec une boucle :
total = 0
for num in range(1, 101):
total += num * num
On peut aussi utiliser la fonction sum qui fait plus rapidement le
travail pour nous en construisant la bonne séquence.
Avec une list comprehension :
total = sum([num * num for num in range(1, 101)])
Avec une generator expression :
total = sum(num * num for num in xrange(1, 101))
Les generator expressions ("genexps") sont comme les list comprehensions, excepté dans leur calcul, les genexps sont fainéantes. Les listcomps calculent l'intégralité du résultat en une seule passe, pour le stocker dans une liste. Les generator expressions calculent une valeur à la fois, lorsqu'elle est nécessaire. C'est particulièrement utile lorsque la séquence est très longue lorsque la liste générée n'est qu'une étape intermédiaire et non le résultat final.
Dans ce cas, on est uniquement intéressé par la somme, on n'a pas besoin de
la liste des résultats intermédiaires. On utilise xrange pour la même
raison, ça génère les valeurs une par une.
Generator Expressions (2)
Par exemple si on doit faire la somme des carrés de plusieurs milliards d'entiers, on va arriver à une saturation de la mémoire avec une list comprehension, mais les generator expressions ne vont avoir aucun problème. Bon ça va prendre un certain temps par contre !
total = sum(num * num
for num in xrange(1, 1000000000))
La différence de syntaxe est que les listcomps ont des crochets, alors que les genexps n'en ont pas. Les generator expressions nécessitent parfois des parenthèses par contre, vous devriez donc toujours les utiliser.
En bref :
- Utilisez une list comprehension lorsque le résultat escompté est la liste.
- Utilisez une generator expression lorsque la liste n'est qu'un résultat intermédiaire.
Voici un récent exemple de ce que j'ai vu au boulot.
On avait besoin d'un dictionnaire qui corresponde aux chiffres des mois (à la fois via des chaînes de caractères et via des integers) au code des mois pour un client. Cela peut être fait avec une ligne de code.
Ça fonctionne de la manière suivante :
- La fonction de référence
dict()prend en argument une liste de paires de clés/valeurs (2-tuples). - On a une liste des codes des mois (chaque code est une simple lettre, et une chaîne de caractères est aussi une simple liste de lettres). On parcours cette liste pour obtenir à la fois le code du mois et l'index.
- Le nombre des mois commence à 1 mais Python commence l'indexation à 0, le nombre des mois correspond dont à index+1.
- On veut avoir la correspondance à la fois avec les nombres et les chaînes
de caractères. On peut utiliser les fonctions
int()etstr()pour ça et itérer dessus.
L'exemple en question :
month_codes = dict((fn(i+1), code)
for i, code in enumerate('FGHJKMNQUVXZ')
for fn in (int, str))
Le résultat obtenu pour month_codes :
{ 1: 'F', 2: 'G', 3: 'H', 4: 'J', ...
'1': 'F', '2': 'G', '3': 'H', '4': 'J', ...}
Ordonner
Il est très simple d'ordonner une liste en Python :
a_list.sort()
(Notez que la liste est ordonnée sur place, la liste originale est ordonnée
et la fonction sort ne retourne pas une liste ou une copie.)
Mais que faire lorsque vous avec une liste de données à ordonner, mais quelle ne s'ordonne pas de manière naturelle ? Par exemple ordonner selon la première colonne, puis la quatrième.
On peut utiliser la fonction de référence sort avec une méthode définie
par nos soins :
def custom_cmp(item1, item2):
returm cmp((item1[1], item1[3]),
(item2[1], item2[3]))
a_list.sort(custom_cmp)
Ça marche, mais c'est extrêmement lent pour les listes énormes.
Ordonner avec DSU
DSU = Decorate-Sort-Undecorate
Note: DSU n'est bien souvent plus nécessaire, cf. section suivante.
Au lieu de créer une fonction de comparaison personnalisée, on crée une liste intermédiaire qui va pourvoir être ordonnée naturellement :
# Decorate:
to_sort = [(item[1], item[3], item)
for item in a_list]
# Sort:
to_sort.sort()
# Undecorate:
a_list = [item[-1] for item in to_sort]
La première ligne crée une liste contenant des tuples, une copie de la valeur à ordonner en premier argument, suivi de la valeur complète de la liste.
La seconde ligne ordonne grâce à la fonction Python, ce qui est très rapide.
La troisième ligne récupère la dernière valeur de la liste une fois ordonnée. Souvenez-vous, cette dernière valeur correspond à l'item complet. On n'utilise plus la partie ayant permis d'ordonner, elle a joué son rôle et n'est plus utile.
C'est un compromis espace mémoire + complexité vs. temps. Plus simple et rapide mais on est obligé de dupliquer la liste originale.
Ordonner avec keys
Python 2.4 a introduit un nouvel argument à la méthode sort des listes,
"key", qui permet de spécifier une fonction à un argument qui est utilisée
pour comparer chaque élément d'une liste avec les autres. Par exemple :
def my_key(item):
return (item[1], item[3])
to_sort.sort(key=my_key)
La fonction my_key va être appelée une fois par item de la liste to_sort.
Vous pouvez utiliser votre propre fonction ou utiliser une fonction existante qui ne prend qu'un seul argument :
str.lowerpour ordonner alphabétiquement sans tenir compte de la casse.lenpour ordonner selon la taille des items (chaînes de caractères ou containers).intoufloatpour ordonner numériquement avec des valeurs qui sont des chaînes de caractères comme "2", "123", "35".
Generators
On a déjà vu les generator expressions. On peut créer nos propres generators, comme des fonctions :
def my_range_generator(stop):
value = 0
while value < stop:
yield value
value += 1
for i in my_range_generator(10):
do_something(i)
Le mot-clé yield transforme une fonction en generator. Lorsque vous
appelez une fonction generator, au lieu d'exécuter le code directement,
Python retourne un objet generator, qui est un itérateur. Il a une méthode
next. Les boucles for appellent la méthode next de l'itérateur,
jusqu'à ce qu'une exception du type StopIteration soit levée.
Vous pouvez lever l'exception StopIteration explicitement ou de manière
implicite comme dans le code ci-dessous.
Les générateurs peuvent simplifier la manière de gérer les séquences/itérateurs, car on n'a pas besoin de créer des listes intermédiaires. Ça ne génère qu'une valeur à la fois.
Voici comment la boucle for fonctionne réellement. Python analyse la
séquence déclarée avec le mot-clé in. Si c'est un simple container (comme
une liste, un tuple, un dictionnaire, un set ou un container défini par
l'utilisateur) Python le converti en itérateur. Si c'est déjà un itérateur,
Python ne fait rien.
Python appelle ensuite de manière itérative la méthode next de
l'itérateur, assignant la valeur retournée au compteur de la boucle (i
dans notre cas), et exécute le code indenté. C'est répété, encore et encore
jusqu'à ce que StopIteration soit levée, ou qu'un break soit exécuté.
Une boucle for peut être dotée d'un else, au sein de laquelle le
code est exécuté si rien ne s'est produit dans la boucle for, mais non
après un break. Cette distinction permet de faire des choses élégantes.
else est rarement utilisé avec la boucle for mais peut s'avérer
très puissant lorsque la logique correspond à ce que vous souhaitez faire.
Par exemple, si on doit vérifier qu'une condition est toujours remplie par tous les items d'une liste :
for item in sequence:
if condition(item):
break
else:
raise Exception('Condition not satisfied.')
Exemple de generator
Filtrer les colonnes vides à partir d'un fichier CSV (ou des items d'une liste) :
def filter_rows(row_iterator):
for row in row_iterator:
if row:
yield row
data_file = open(path, 'rb')
irows = filter_rows(csv.reader(data_file))

