Python love: les listes en intention (partie 2)

En première partie, nous avons vu les bases des listes en intention. Mais elles ont encore beaucoup de choses à offrir. Même si vous les utilisez depuis quelques temps, lisez la suite, vous pourriez bien apprendre quelque chose.

Les expressions génératrices

Il y a plus encore !

Quand vous faites ceci:

>>> nombres = [sum(range(nombre)) for nombre in range(0, 10, 2)]
>>> for nombre in nombres:
...     print nombre
...
0
1
6
15
28

La liste nombres est créé en mémoire.

Si la liste est petite, ce n’est pas un problème. Mais si la liste est grande, par exemple si c’est un fichier complet, cela peut devenir très consommateur de RAM.

Il existe un moyen de palier cela:

>>> nombres = (sum(range(nombre)) for nombre in range(0, 10, 2))
>>> for nombre in nombres:
...     print nombre
...
0
1
6
15
28

Vous ne voyez pas la différence ? Regardez de plus prêt la première ligne: les [] ont été remplacés par des parenthèses. Tout le reste de la syntaxe est la même.

Ce petit détails change absolument tout:

>>> [sum(range(nombre)) for nombre in range(0, 10, 2)]
[0, 1, 6, 15, 28]
>>> (sum(range(nombre)) for nombre in range(0, 10, 2))
<generator object <genexpr> at 0x7f07bac94be0>

[] créé une liste.
() créé un générateur.

Un générateur ressemble beaucoup une liste, on peut l’utiliser de la même manière dans une boucle for. La différence principale est que le générateur ne peut être lu qu’une seule fois. Si vous bouclez dessus une seconde fois, il sera vide:

>>> nombres = (sum(range(nombre)) for nombre in range(0, 10, 2))
>>> for nombre in nombres:
...     print nombre
... 
0
1
6
15
28
>>> for nombre in nombres: # ceci n'affiche rien !
...     print nombre
...

La raison à cela est que le générateur ne contient pas toutes la valeurs de la liste. Il les genère.

Il calcule chaque valeur une à une, à la volée, quand la boucle for lui demande. Il calcule la première valeur, puis l’oublie, puis la deuxième, puis l’oublie, etc. Jusqu’à la dernière.

Cela signie qu’on ne peut pas utiliser un générateur pour récupérer un élément en particulier:

>>> liste = [sum(range(nombre)) for nombre in range(0, 10, 2)]
>>> liste[0]
0
>>> generateur = (sum(range(nombre)) for nombre in range(0, 10, 2))
>>> generateur[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable

On utilise les générateurs partout où l’on a besoin d’une liste, mais qu’on ne souhaite pas stocker toute la liste en mémoire, et qu’on pense lire la liste une seule fois.

Il y a bien plus à dire sur les générateurs, mais restons en là pour le moment.

Il n’y a pas que les listes dans la vie

Python a une idée bien plus large de l’itération que les listes. En fait, tout objet “itérable” peut être utilisé dans une boule for:

>>> une_liste = [1, 2, 3]
>>> for item in une_liste:
...     print item
... 
1
2
3
>>> un_tuple = (1, 2, 3)
>>> for item in un_tuple: 
...     print item
... 
1
2
3
>>> un_dictionnaire = {'moi': 'tarzan', 'toi': 'jane'}
>>> for key in un_dictionnaire:
...     print key
... 
moi
toi
>>> une_chaine_de_caracteres = "yabadabado"
>>> for lettre in une_chaine_de_caracteres:
...     print lettre
... 
y
a
b
a
d
a
b
a
d
o
>>> un_fichier = open('fichier_de_test.txt')
>>> for line in un_fichier:
...     print line
... 
ceci
est
un
test

Etre iterable est un concept à part entière en Python.

Et il se trouve que les listes en intention acceptent n’importe quel itérable, pas juste le listes.

Tout comme les expressions génératrices.

Et plein de fonctions acceptent n’importe quel itérable. tuple(), liste(), join(), sum(), etc. Du coup on peut faire un tas de combos qui à faire se pamer un fan de Tekken:

>>> [str(sum(range(int(nombre)))) for nombre in "123456789"]
['0', '1', '3', '6', '10', '15', '21', '28', '36']
>>> ', '.join([str(sum(range(int(nombre)))) for nombre in "123456789"])
'0, 1, 3, 6, 10, 15, 21, 28, 36'
>>> ', '.join(str(sum(range(int(nombre)))) for nombre in "123456789")
'0, 1, 3, 6, 10, 15, 21, 28, 36'

Notez sur la dernière ligne que l’on peut carrément supprimer les [], ne pas rajouter de parenthèses, et ça marche toujours. L’expression retourne un générateur, passé à en paramètre à join().

On peut aussi chainer tout ça, les unes à la suite des autres et créer un gigantesque système de pipes à coup de générateurs, pluggable sur n’importe quel itérable.

Par exemple, vous voulez filtrer le contenu d’un fichier ?

>>> f = open('fichier_de_test.txt')
>>> lignes_non_vides = (line for line in f if line.strip())
>>> mot = "ni"
>>> lignes_qui_contienne_un_mot = (l for l in lignes_non_vides if mot in l)
>>> lignes_qui_ne_finissent_pas_par_un_point = (l for l in lignes_qui_ne_finissent_pas_par_un_point if not l.endswith('.'))
>>> lignes_numerotees = enumerate(lignes_qui_ne_finissent_pas_par_un_point)

Afficher toutes les lignes non vides, du fichier fichier_de_test.txt, qui contiennent “ni”, ordonnées par ordre alphabetique, et numérotées:

>>> for numero, line in lignes_numerotees:
...     print "%s - %s" % (numero, line)
...
1 - Ni Dieu, ni maître
2 - Les chevaliers qui disent "ni"
3 - Con nichon ahhhhhhhh. Ca veut dire bonjour en japonais

À aucun moment l’intégralité du fichier n’est stocké en mémoire. Toutes les lignes sont traitées une par une.

Il y a encore énormément à dire sur les itérables mais je vous plutôt vous laisser digérer ce gros morceau.

Bonus 1: Les dictionnaires et les set en intentions

A partir de Python 2.7, les listes en intentions ont également de nouveaux amis, les dictionnaires et les sets en intentions.

Auparavant, pour réer un dictionnaire à la volée, il fallait faire ceci:

>>> liste_de_tuples = [(str(i), i) for i in range(10)]
>>> liste_de_tuples
[('0', 0), ('1', 1), ('2', 2), ('3', 3), ('4', 4), ('5', 5), ('6', 6), ('7', 7), ('8', 8), ('9', 9)]
>>> dict(liste_de_tuples)
{'1': 1, '0': 0, '3': 3, '2': 2, '5': 5, '4': 4, '7': 7, '6': 6, '9': 9, '8': 8}

Maintenant vous pouvez faire quelque chose de plus direct:

>>> {str(i): i for i in range(10)}
{'1': 1, '0': 0, '3': 3, '2': 2, '5': 5, '4': 4, '7': 7, '6': 6, '9': 9, '8': 8}

Idem pour les sets. Pour rappel, les sets sont des itérables non ordonnés, sans doublons. Depuis Python 2.7, ont peut les créer ainsi:

>>> {1, 2, 3, 4, 4, 4, 4}
set([1, 2, 3, 4])

Avant, pour créer un set en une ligne, on faisait:

>>> set(i*i for i in range(10))
set([0, 1, 4, 81, 64, 9, 16, 49, 25, 36])

Maintenant on peut faire:

>>> {i*i for i in range(10)}
set([0, 1, 4, 81, 64, 9, 16, 49, 25, 36])

Bonus 2: nested comprehensions lists

Les listes en intentions peuvent contenir des listes en intentions de 2 manières.

La première est classique, et permet de créer des listes de listes (ou de générateurs):

>>> [[i*i for i in range(x)] for x in range(5)]
[[], [0], [0, 1], [0, 1, 4], [0, 1, 4, 9]]

La deuxième est beaucoup moins intuitive, permet d’aplatir une squence de sequences. L’inverse en quelque sorte.

Par exemple, vous avez ceci:

[[], [0], [0, 1], [0, 1, 4], [0, 1, 4, 9]]

Vous voulez obtenir celà:

[0, 0, 1, 0, 1, 4, 0, 1, 4, 9]

Hop:

>>> liste_de_listes = [[], [0], [0, 1], [0, 1, 4], [0, 1, 4, 9]]
>>> [element_de_sousliste for sousliste in liste_de_listes for element_de_sousliste in sousliste]
[0, 0, 1, 0, 1, 4, 0, 1, 4, 9]

Ca parait verbeux parceque les noms sont noms, mais en vérité c’est plutôt court:

>>> [x for i in liste_de_listes for x in i]
[0, 0, 1, 0, 1, 4, 0, 1, 4, 9]

La syntaxe n’est pas du tout intuitive, et pour se souvenir de l’odre des choses, voici une astuce visuelle. Formez ainsi la liste dans votre esprit. Ceci:

[element_de_sousliste for sousliste in liste_de_listes for element_de_sousliste in sousliste]

Est en fait:

[element_de_sousliste <= truc à mettre dans la nouvelle liste
    for sousliste in liste_de_listes <= ordre normal d'une double boucle for
        for element_de_sousliste in sousliste]

Car une double boucle for serait ainsi faite:

nouvelle_liste = []
for sousliste in liste_de_listes:
    for element_de_sousliste in sousliste:
        nouvelle_liste.append(element_de_sousliste)

C’est bon, vous pouvez débranchez votre cerveau et retourner sur Youporn.

No related posts.

flattr this!

5 comments

  1. Bonjour. J’adore votre site avec plein de trucs trop biens pour faire du VRAI python.
    Par contre j’ai une question sur la ligne
    lignes_ordonnees = sorted(lignes_qui_ne_finissent_pas_par_un_point)

    Vous dites que À aucun moment l’intégralité du fichier n’est stocké en mémoire.. Sans regarder l’implémentation en C, je n’ai pas l’impression que sorted puisse fonctionner sans créer la liste entière en mémoire.
    Pouvez-vous m’éclairer là dessus ?

  2. Oulalala, n’importe quoi le Sam !

    sorted ne retourne pas du tout une generateur, en écrivant l’article j’avais utilisé reversed et je sais plus pourquoi j’ai switché en route:

    >>> sorted(xrange(100))
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
    >>> enumerate(xrange(100))
    <enumerate object at 0xb7737e8c>
    >>> reversed(xrange(100000000))
    <rangeiterator object at 0xb7725aa0>

    C’est une bêtiserie, d’autant que sorted utilise une implémentation optimisé de quick sort qui, par principe, contient la liste en mémoire.

    Vais faire un article sur l’importance de dire des conneries en publique en programmation.

    En attendant je corrige ce truc.

    EDIT: je me souviens pourquoi j’avais viré reversed, c’est parceque dans le cas du traitement d’un fichier, reversed doit attendre qu’on arrive à la fin du fichier pour inverser la lecture, donc aussi mettre toute la liste en mémoire. Un autre mauvais exemple. Je vais en mettre un autre.

    EDIT 2: ok je bouge le enumerate, car lui aussi retourne un générateur, mais celui-ci est garantie de ne pas mettre la liste en mémoire. Le but étant de démontrer qu’il y a des built-in Python qui retournent des générateurs.

  3. J’en profite pour rappeler qu’il faut toujours vérifier ce que nous postons (ça s’applique à tout internet cela dit). Nous avons un large historique de stupidités à notre actif, et un fort potentiel d’innovaton pour le futur.

    Pour les articles sur Python c’est facile, lancez votre shell, et testez les exemples. C’est la meilleure manière d’apprendre de toute façon.


  4. >>> sorted(xrange(1000000000))
    Traceback (most recent call last):
    File "", line 1, in
    MemoryError

    C’est ce que j’avais essayé avant de poster mon commentaire ;)

  5. Pourquoi y a du faux python ? :)

    En fait on essais surtout de parler des choses qui nous arrive en tant que developpeur AYANT des sites webs, ce qui n’a rien à voir avec un developpeur classique. A comprendre que quelqu’un qui a un site web va raisonner differement d’un dev pur qui sort de l’école et pose son cul sur une chaise en attendant le cahier des charges, on a pas les mêmes priorités, il faut faire des concessions, trouver des astuces etc, un dev qui n’a pas de site va recopier le man page et sortir de la merde (je m’excuse mais j’aime pas les devs qui ont pas de sites web, ils sont souvent à 1000 années lumière de comprendre les priorités quand on developpe un site)

    Alors à chaque problème rencontré sur nos sites on poste un petit article en rapport, perso je trouve ça plus concret et plus interressant.

    Merci de nous lire ceci dit ;)

Flux RSS des commentaires

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">

Jouer à mario en attendant que les autres répondent