Listes: Acte II

Slicing

Jusqu’ici on a vu comment récupérer individuellement des éléments d’un tuple, d’une str ou d’une list via l’opérateur []. Mais dans de nombreux cas, on préférerait faire une sélection de plusieurs éléments simultanément.

Une réponse partielle à cette problématique est le slicing, il permet de récupérer une copie d’une partie du conteneur.

Notez que si l’on ne met pas d’indice avant le : c’est comme si on mettait 0 donc on commence au premier élément. Si on ne le met pas après :, c’est comme si on insérait la longueur (on va inclure jusqu’au dernier élément)

>>> nombres = [0, 1, 2, 3]
>>> nombres[:2]
[0, 1]
>>> nombres[2:]
[2, 3]
>>> nombres[:]
[0, 1, 2, 3]

REMARQUE le fait de ne pas inclure l’élément contenant l’indice de droite permet de faire des découpages plus facilement.

>>> nombres = [1.5, 2.5, 3.5, 4.5, 5.5, 6.5]
>>> debut, milieu, fin = nombres[:2], nombres[2:4], nombres[4:]
>>> debut
[1.5, 2.5]
>>> milieu
[3.5, 4.5]
>>> fin
[5.5, 6.5]

Par exemple si on veut prendre les éléments d’indices pairs et impairs on peut faire:

>>> nombres = list(reversed(range(21)))
>>> nombres
[20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> pairs, impairs = nombres[::2], nombres[1::2]
>>> pairs
[20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 0]
>>> impairs
[19, 17, 15, 13, 11, 9, 7, 5, 3, 1]

REMARQUE ici encore si le premier nombre est absent on commence à l’indice 0, et si le deuxième n’est pas là on va jusqu’au bout de la liste.

ATTENTION Un point de détail important est que l’utilisation de slicing produit une copie:

>>> originale = [1, 2, 3]
>>> nouvelle = originale[:]
>>> originale is nouvelle
False

mais cette copie est superficielle:

>>> originale = [(1, 2), (3, 4)]
>>> nouvelle = originale[:]
>>> nouvelle is originale
False
>>> nouvelle[0] is originale[0]
True

Dans le doute on n’hésitera pas à utiliser le module copy déjà vu pour faire des copies!

Compréhension

Le slicing permet de faire des extractions d’un conteneur, mais les indices doivent suivre une certaine régularité. On a une façon plus puissante pour faire des extractions (et aussi des transformations) à partir non seulement d’un conteneur mais en fait d’un itérable.

Il s’agit des compréhensions. Elle permettent d’une certaine façon de décrire le résultat voulu plutôt que d’expliciter sa construction. La syntaxe générale est

[ EXPRESSION for ELEMENT in ITERABLE if EXPRESSION_BOOLEENNE]

REMARQUE on fera référence à la partie EPRESSION en parlant de transformation, et à la partie if EXPRESSION_BOOLEENNE en parlant de filtrage.

REMARQUE autant EXPRESSION que EXPRESSION_BOOLEENNE utilise habituellement ELEMENT.

REMARQUE la partie if ... est optionnelle.

On va voir quelques exemples précis:

>>> carres = [nombre ** 2 for nombre in range(11)]
>>> carres
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> impairs = [nombre for nombre in range(21) if nombre % 2 == 1]
>>> impairs
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
>>> total = [nombre ** 2 for nombre in range(20) if nombre % 3 == 0]
>>> total
[0, 9, 36, 81, 144, 225, 324]

La syntaxe de construction par compréhension est à opposer à celle que l’on a vu jusqu’à présent qui est dite par accumulation. On reprend les trois exemples précédents par accumulation pour comparer.

>>> carres = list()
>>> for nombre in range(11):
...     carres.append(nombre ** 2)
...
>>> carres
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> impairs = list()
>>> for nombre in range(21):
...     if nombre % 2 == 1:
...             impairs.append(nombre)
...
>>> impairs
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
>>> total = list()
>>> for nombre in  range(20):
...     if nombre % 3 == 0:
...             total.append(nombre ** 2)
...
>>> total
[0, 9, 36, 81, 144, 225, 324]

REMARQUE on voit que les compréhension sont plus concises. Ce n’est pas, en tant que tel, obligatoirement positif, mais cela permet de ne pas étaler une idée sur plusieurs lignes. Et dans ce cas, cela peut effectivement améliorer la lisibilité.

ATTENTION un des risques des compréhensions et d’avoir des logiques trop compliquées pour les parties transformation et filtrage. On n’hésitera pas à terme à créer des fonctions dédiées pour ces deux parties.

Syntaxe avancée

Mentionnons pour finir deux mécanismes pratiques, mais dont l’abus peut s’avérer apocalyptique pour la lisibilité.

On l’utilise principalement dans des expressions booléennes, pour ne pas avoir à évaluer deux fois la même expression. Par exemple:

>>> restes = [reste for nombre in range(20) if (reste := nombre % 3) != 1]
>>> restes
[0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0]

ou

>>> ls = list(range(5))
>>> if  (n := len(ls)) > 3:
...     print(f"la liste a {n} éléments")
...
la liste a 5 éléments

REMARQUE les exemples ci-dessus sont très académiques. On réserverait plutôt le mécanisme au cas où l’évaluation est très couteuse en temps de calcul. Ou alors si l’évaluation n’est pas déterministe: c’est à dire qu’une deuxième évaluation pourrait être différente (à cause d’utilisation de ressources réseau, de changement dans un fichier, de l’utilisation d’un générateur de nombres aléatoires…)

ATTENTION quitte à se répéter: ces mécanismes ont une capacité importante à produire du code ILLISIBLE. On les utilisera donc avec parcimonie.

Pour se convaincre du danger, deviner le résultat des lignes suivantes (puis vérifier votre conjecture dans l’interpréteur).

>>> resultat = (x := 123 % 11) + 11 * (y := 123 // 11) + (x - 2) * (y - 11)
>>> ls = [[x + y for y in range(z) if (y - z) % 3 == 2] for x in range(20) for z in range(x * 4) if x % 2 == 1]

Exercices

  1. Construisez la liste des cubes des multiples de 5 entre 1 et 100.
  2. En supposant donnée nombres: list[int] découpez la en trois sous listes suivant le reste de la division par 3 des indices.
  3. On suppose donnée originale: list, utilisez enumerate et une compréhension pour obtenir le même résultat que originale[13:57:4].
  4. On suppose donnée message: str, donner un code permettant de ne conserver que les lettres entre g et r! (on pourra utilisez des compréhensions et la fonction ord)

Pour aller plus loin

Corrections

  1. On propose deux solutions, la première avec un filtrage la deuxième avec un slicing.

    >>> cubes = [nombre ** 3 for nombre in range(1, 101) if nombre % 5 == 0]
    >>> cubes
    [125, 1000, 3375, 8000, 15625, 27000, 42875, 64000, 91125, 125000, 166375, 216000, 274625, 343000, 421875, 512000, 614125, 729000, 857375, 1000000]
    >>> cubes_alternatif = [nombre ** 3 for nombre in range(5, 101, 5)]
    >>> cubes_alternatif
    [125, 1000, 3375, 8000, 15625, 27000, 42875, 64000, 91125, 125000, 166375, 216000, 274625, 343000, 421875, 512000, 614125, 729000, 857375, 1000000]

    Notez que la première est à la fois plus générale et plus lisible. La seconde a pour seul vague intérêt d’exploiter la structure du problème et de ce fait d’être plus efficace mais c’est ici totalement anecdotique vue la taille des listes.

  2. On utilise la régularité des indices pour faire du slicing (on prend 1 élément sur 3 de fait)

    >>> nombres = [1, -7, -5, 2, -8, 6, 6, -10, 10, 9, -7, -7, -3, 5, 9, 4, 7, 4, -1, 3]
    >>> reste_0, reste_1, reste_2 = nombres[::3], nombres[1::3], nombres[2::3]
    >>> reste_0
    [1, 2, 6, 9, -3, 4, -1]
    >>> reste_1
    [-7, -8, -10, -7, 5, 7, 3]
    >>> reste_2
    [-5, 6, 10, -7, 9, 4]
  3. Le code suivant montre que même si les compréhensions sont plus générales, le slicing a tout à fait sa place, tant d’ailleurs niveau lisibilité que niveau efficacité.

    >>> originale = [-8, -8, 4, 6, 7, 2, 9, 0, -3, -4, -9, -10, 3, 0, -3, 4, -2, -8, -7, 8, 8, -3, 9, 4, -4, 5, 10, 9, -9, 6, 0, -1, 6, 10, 9, -7, -1, 8, -3, -6, -8, -6, -3, 4, -2, -3, 10, -5, 5, -7, 5, -6, -7, 5, 10, -1, -6, 0, -5, 6, -3, 3, -7, 5, -3, -9, 10, 7, -4, -1, -2, -8, 3, 3, -1, -6, -1, -6, -3, 9, -2, -9, -7, 0, 10, 6, -2, 10, -5, -3, -5, -3, -3, -10, 9, 6, 7, -5, 10, 9]
    >>> resultat = [nombre for indice, nombre in enumerate(originale) if (indice - 13) % 4 == 0 and 13 <= indice < 57]
    >>> attendu = originale[13:57:4]
    >>> resultat == attendu
    True
  4. On utilise ici le fait que l’unicode est séquentiel, donc les lettres entre h et r ont des unicodes entres ceux des ces lettres.

    >>> message = "the quick brown fox jumps over the lazy dog"
    >>> lettres = [lettre for lettre in message if ord("g") <= ord(lettre) <= ord("r")]
    >>> resultat = "".join(lettres)
    >>> resultat
    'hqikronojmporhlog'