PROJET AUTOBLOG


Gordon

source: Gordon

⇐ retour index

Listes en compréhension en Python

mercredi 5 mars 2014 à 00:00

J’aime principalement deux choses dans le langage Python : la redoutable simplicité de sa syntaxe, et l’incroyable puissance des listes en compréhension, permettant d’effectuer des traitements en une seule ligne imbuvable. Oui, c’est parfaitement contraire au premier point. Je vais donc revenir sur ces listes en compréhensions.

De quoi parle-t-on ?

Les listes en compréhension sont une syntaxe présente dans le langage Python (entre autres) permettant de filtrer un itérable (comme une liste). En gros, cela permet l’écriture d’une boucle for dont la finalité est de créer une liste. Un exemple sera plus parlant.

resultat = []
for i in range(10):
    resultat.append(i*2)

Cette syntaxe classique utilise 3 lignes pour générer la simple liste [0,2,4,6,8,10,12,14,16,18,20]. Voyons maintenant comment écrire cela autrement :

resultat = [i*2 for i in range(10)]

Voila. Rien de plus. Nous arrivons au même résultat avec une écriture bien plus concise. Il est possible de compléter l’exemple précédent :

resultat = []
for i in range(10):
    if(i % 2 == 0):
        resultat.append(i)

On itère i de 0 à 9, et on insère i dans resultat si celui-ci est pair (c’est à dire si le résultat de sa division par 2 est nul).

Voyons maintenant la version en liste en compréhension :

resultat = [i for i in range(10) if i % 2 == 0]

On peut donc, grâce à la version verbeuse de l’expression, isoler les différentes parties :

La puissance des listes en compréhension est incroyable. Pensez que l’itérable source de votre liste en compréhension peut lui aussi être une liste en compréhension !

Expressions génératrices

Si vous ne connaissez pas les générateurs en Python, il s’agit de structures itérables dont la valeur est calculée au moment où on tente d’y accéder, et non pas à l’assignation. Ce qui permet d’itérer sur de très gros volumes de données, mais également d’itérer à l’infini sur une valeur.

>>> def sq(n):
...     print('sq(%d)' % d) # on affiche quelque chose à chaque exécution
...     return n**2
...
>>> l = [sq(i) for i in range(10)]
sq(0)
sq(1)
sq(2)
sq(3)
sq(4)
sq(5)
sq(6)
sq(7)
sq(8)
sq(9)

Comme on le constate, avec une simple liste en compréhension, la fonction sq() est appelée à l’assignation de la liste, car les valeurs sont calculées à ce moment. Ce n’est pas le cas des expressions génératrices.

>>> g = (sq(i) for i in range(10))

Rien n’est affiché. Notre fonction sq() n’est donc pas appelée. Elle le sera à chaque fois qu’on cherchera à accéder à un élément du générateur.

>>> for i in g:
...     print(i)
... 
sq(0)
0
sq(1)
1
sq(2)
4
sq(3)
9
sq(4)
16
sq(5)
25
sq(6)
36
sq(7)
49
sq(8)
64
sq(9)
81

Les lignes « sq(×) » sont le signe que notre fonction sq() est exécutée à ce moment. Et donc, en cas de données lourdes, on ne charge pas tout en mémoire instantanément.

La seule chose qui distingue une expression génératrice d’une liste en compréhension, syntaxiquement parlant, est simplement l’usage de parenthèses autour de l’expression au lieu de crochets.

Sets en compréhension

Enfin, et parce que je préfère évoquer toutes les possibilités de cette syntaxe, sachez qu’il est possible de générer un set (c’est à dire une liste dédoublonnée) à partir d’une liste en compréhension. Il suffit pour cela d’utiliser les accolades au lieu de crochets autour de l’expression.

>>> s = [n % 5 for n in range(10)] # liste en compréhension
>>> s
[0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
>>> s = {n % 5 for n in range(10)} # set en compréhension, sans doublon
>>> s
{0, 1, 2, 3, 4}

Un exemple ?

La raison profonde pour laquelle j’ai voulu écrire cet article est le besoin récent que j’ai eu de convertir une chaîne binaire en texte, par conversion des octets en nombres décimaux, puis correspondance dans la table ascii. Malgré l’existence de nombreux convertisseurs en ligne (j’en ai moi-même écrit), je me suis dit qu’écrire un convertisseur en une ligne serait amusant, le tout sous les yeux d’une amie. Et donc, voici :

>>> s = '01010000011010010110111001101011011010010110010100100000010100000110100101100101001000000110100101110011001000000111010001101000011001010010000001100010011001010111001101110100'
>>> print(''.join([chr(int(b, 2)) for b in [s[i:i+8] for i in range(0, len(s), 8)]]))
Pinkie Pie is the best

Voilà.

Bon, ok, je vous fais la version longue et commentée :

s = '01010000011010010110111001101011011010010110010100100000010100000110100101100101001000000110100101110011001000000111010001101000011001010010000001100010011001010111001101110100'
conversion = [] # on stocke le résultat dans un tableau, qu’on convertira
                # ensuite en chaîne

# commençons par découper notre chaîne en octets (8 bits)
octets = []
# on doit itérer (taille de la chaîne / 8) arrondi au supérieur (au cas où)
for i in range( 0, len(s), 8 ):
    octets.append(s[i:i+8]) # vivent les slices d’itérable : on découpe
                            # à partir de i caractères jusqu’à 8 de
                            # plus au maximum
# on a maintenant nos octets séparés. Il ne reste plus qu’à les convertir en
# décimaux, puis récupérer la valeur de la table ascii correspondante
for octet in octets:
    octet_dec = int(octet, 2) # pour convertir à partir de la base 2
    conversion.append( chr( octet_dec ) )

print( ''.join( conversion ) ) # ENFIN !

Vous ne trouvez pas que la première version est plus, disons, succinte ?

[edit] Rogdham m’a suggéré une amélioration du convertisseur binaire