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.
La version la plus élémentaire permet de récupérer une suite contigüe d’éléments en donnant l’indice de démarrage, et le premier indice non inclus.
>>> ma_liste = list("abcde")
>>> for indice, element in enumerate(ma_liste):
... print(f"{indice} -> {element}")
...
0 -> a
1 -> b
2 -> c
3 -> d
4 -> e
>>> ma_liste[1:3]
['b', 'c']
>>> ma_liste[0:4]
['a', 'b', 'c', 'd']
ATTENTION notez bien que lorsque on a fait [1:3]
l’élément d’indice 3
n’a pas été inclus.
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]
On a une version plus raffinée de la syntaxe avec un :
et un nombre supplémentaire. Il permet de ne pas prendre séquentiellement les éléments mais de faire des sauts:
>>> nombres = list(range(10))
>>> nombres
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> nombres[1:6:2]
[1, 3, 5]
La syntaxe est donc CONTENEUR[a:b:c]
avec a
l’indice de début, b
l’indice de fin (non inclus) et c
l’espacement entre les indices sélectionnés. Mathématiquement cela signifie qu’on prend les indices i
vérifiant à la fois et .
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!
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.
Mentionnons pour finir deux mécanismes pratiques, mais dont l’abus peut s’avérer apocalyptique pour la lisibilité.
Les compréhensions sont composables et les boucles multiples
>>> produits = [a * b for a in range(10) for b in range(10)]
>>> produits
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 0, 6, 12, 18, 24, 30, 36, 42, 48, 54, 0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 0, 9, 18, 27, 36, 45, 54, 63, 72, 81]
ce qui en accumulation s’écrit
>>> produits = list()
>>> for a in range(10):
... for b in range(10):
... produits.append(a * b)
...
>>> produits
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 0, 6, 12, 18, 24, 30, 36, 42, 48, 54, 0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 0, 9, 18, 27, 36, 45, 54, 63, 72, 81]
Et pour des list
de list
>>> tableau = [[(i,j) for j in range(1, 5)] for i in range(1, 4)]
>>> tableau
[[(1, 1), (1, 2), (1, 3), (1, 4)], [(2, 1), (2, 2), (2, 3), (2, 4)], [(3, 1), (3, 2), (3, 3), (3, 4)]]
ce qui donne en accumulation
>>> tableau = list()
>>> for i in range(1, 4):
... nouveau = list()
... for j in range(1, 5):
... nouveau.append((i,j))
... tableau.append(nouveau)
...
>>> tableau
[[(1, 1), (1, 2), (1, 3), (1, 4)], [(2, 1), (2, 2), (2, 3), (2, 4)], [(3, 1), (3, 2), (3, 3), (3, 4)]]
On a, à partir de python 3.8, un opérateur d’affectation/expression (dit aussi walrus operator) il s’agit de :=
. On peut ainsi affecter des variables à l’intérieur même d’expressions.
>>> x = (y:= 5 % 2) ** 3
>>> x
1
>>> y
1
Ici quand on a évalué l’expression à droite de x =
, l’évaluation de l’expression entre parenthèses, entraine en plus l’affectation du résultat intermédiaire à la variable y
.
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]
5
entre 1
et 100
.nombres: list[int]
découpez la en trois sous listes suivant le reste de la division par 3
des indices.originale: list
, utilisez enumerate
et une compréhension pour obtenir le même résultat que originale[13:57:4]
.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 le slicing on peut consulter:
>>> help("SLICINGS")
En ce qui concerne le walrus operator, on pourra regarder le PEP correspondant. Notez que PEP signifie Python Enhancement Proposal il s’agit des documents proposant de nouvelles fonctionnalités/syntaxes pour les futures versions de python. (Le débat pour ce PEP précis a été particulièrement houleux et a débouché sur l’auto mise en retrait du créateur du langage!)
Pour les compréhensions, on peut là aussi consulter le PEP ou l’aide en ligne
On pourra explorer l’ensemble des PEP qui ont l’avantage d’argumenter sur l’intérêt des mécanismes proposés.
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.
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]
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
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'