Outils personnels
Vous êtes ici : Accueil Python Bonnes pratiques et astuces Python Les listes et tuples

Les listes et tuples

Par David Goodger - Dernière modification 17/05/2008 11:23
Contributeurs : David Larlet
CC BY-SA

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() et str() 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.lower pour ordonner alphabétiquement sans tenir compte de la casse.
  • len pour ordonner selon la taille des items (chaînes de caractères ou containers).
  • int ou float pour 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))
Actions sur le document