Python love: les listes en intension (partie 2)


En première partie, nous avons vu les bases des listes en intension. 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éée 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 pallier à 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ès 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étail 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ée une liste.
() crée un générateur.

Un générateur ressemble beaucoup à une liste : c’est un itérable, donc 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 les valeurs de la liste. Il les génè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 signifie qu’on ne peut pas demander à un générateur un élément à un index en particulier:

>>> liste = [sum(range(nombre)) for nombre in range(0, 10, 2)]
>>> liste[0] # donne moi l'élément à l'index 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, qui est une notion très puissante et importante en Python. Si vous avez envie d’en savoir plus, par exemple si le mot clé yield vous intrigue, il y a un article dédié.

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 boucle 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)
...
b'ceci'
b'est'
b'un'
b'test'

Être itérable est un concept à part entière en Python.

Et il se trouve que les listes en intension acceptent n’importe quel itérable, pas juste les 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 à faire se pâmer 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', encoding="ascii")
>>> lignes_non_vides = (line for line in f if line.strip())
>>> mot = "ni"
>>> lignes_qui_contiennent_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_contiennent_un_mot 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” 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 vais plutôt vous laisser digérer ce gros morceau.

Bonus 1: Les dictionnaires et les sets en intension

C’est le même principe pour les dictionnaires, on doit juste entourer de {} et séparer clé et valeur par :.

>>> {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 :

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

Ne confondez pas avec un dico : il n’y a que des valeurs, pas de clés.

On peut faire des sets en intension, la syntaxe ressemble à celle des dicos, mais sans le : :

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

Bonus 2: nested comprehension lists

Les listes en intension peuvent contenir des listes en intension 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 de combiner plusieurs boucles. On peut par exemple aplatir une séquence de séquences. L’inverse en quelque sorte.

Ainsi, sous avez ceci:

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

Vous voulez obtenir cela:

[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]

On peut aussi l’utiliser pour combiner des éléments de deux sources différentes.

Récupérer toutes les combinaisons de couleurs et formes :

>>> colors = ['red', 'green', 'yellow']
>>> forms = ['circle', 'square', 'triangle']
>>> [{'color': c, 'form': f} for c in colors for f in forms]
[{'color': 'red', 'form': 'circle'},
{'color': 'red', 'form': 'square'},
{'color': 'red', 'form': 'triangle'},
{'color': 'green', 'form': 'circle'},
{'color': 'green', 'form': 'square'},
{'color': 'green', 'form': 'triangle'},
{'color': 'yellow', 'form': 'circle'},
{'color': 'yellow', 'form': 'square'},
{'color': 'yellow', 'form': 'triangle'}]

La syntaxe n’est pas du tout intuitive, et pour se souvenir de l’ordre 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.

25 thoughts on “Python love: les listes en intension (partie 2)

  • Hugo

    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 ?

    • Max

      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 ;)

  • Sam Post author

    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.

  • Sam Post author

    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.

  • Hugo


    >>> 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 ;)

  • n_arno

    Nécro-commentaire, mais bien content du bonus 2 qui explique la syntaxe de ce que j’utilise pour aplatir des listes de listes sans avoir compris pourquoi les for sont dans cet ordre

  • furankun

    Ca fait plusieurs fois que je relis cette belle paire d’articles et ils me retournent toujours autant le cerveau. Mais c’est vraiment super formateur!
    Je sais qu’on n’est pas sur stackoverflow mais j’ai quand même une question dont je n’arrive pas à me débrouiller, à voir si ça vous intéresse:
    j’ai une liste de listes d’index complémentaires (chaque sous-liste contient des éléments qui ne se trouvent pas dans les autres), du genre
    mesIndex = [[1, 1, 0, 0, 0, 1], [0, 0, 1, 0, 1, 0], [0, 0, 0, 1, 0, 1]]
    j’ai une autre liste de trucs, mettons
    mesTrucs = [‘Saucisse’,’Canard’,’Gomme’,’Truc4′,’Touillette’,’Bas_resille’]

    est-ce que c’est possible de comment on fait avec les listes en intention pour filtrer mesTrucs avec mesIndex et placer ça dans une liste?
    ce que je voudrais c’est:
    monRes = [‘Saucisse’,’Saucisse’,’Canard’,’Gomme’,’Canard’,’Gomme’]

    J’ai essayé
    for ii in range(len(mesTrucs)):
    monRes[ii] = [mesTrucs[ii] for x in mesIndex[ii][0] if x == 1]

    pour au moins obtenir monRes = [‘Saucisse’,’Saucisse’,None,None,None,None]
    mais je me confronte à mes limites de pythonoob:”TypeError: ‘int’ object is not iterable”

  • Sam Post author

    Hello furankun,

    Je n’ai pas compris comment passer de mesTrucs à MonRes, faut que tu expliques plus.

    Et effectivement, les commentaires sont pas idéaux pour ce genre de question, un forum comme celui de l’afpy est plus indiqué.

  • furankun

    Salut,
    oui j’ai bien conscience que c’est pas le mieux. Juste pour répondre à ta question (quand même): il s’agirait de placer dans MonRes le résultat d’une opération logique entre mesTrucs et mesIndex, pour chaque élément dans mesTrucs et chaque sous-liste dans mesIndex (je sens que je ne suis pas beaucoup plus clair là…).
    En gros:
    étape 1
    monRes = ‘Saucisse’ & [1, 1, 0, 0, 0, 1] soit
    monRes = [‘Saucisse’,’Saucisse’, None, None, None, ‘Saucisse’]
    étape 2
    monRes = monRes & [‘Canard’ & [0, 0, 1, 0, 1, 0]] soit
    monRes = [‘Saucisse’,’Saucisse’, ‘Canard’, None, ‘Canard’, ‘Saucisse’]

    etc

    c’est pas du code, c’est pour avoir le principe.

  • Sam Post author

    Il manque encore des éléments, mais en présupposant que tu veuilles une accumulation simple et qu’il y a autant d’index que de trucs, tu peux commencer comme ça :

    mes_index = [[1, 1, 0, 0, 0, 1], [0, 0, 1, 0, 1, 0], [0, 0, 0, 1, 0, 1],  [0, 0, 0, 1, 0, 1], [0, 0, 0, 1, 0, 1],[0, 0, 0, 1, 0, 1],]
    mes_trucs = ['Saucisse','Canard','Gomme','Truc4','Touillette','Bas_resille']
     
     
    for index, truc : zip(mes_index, mes_trucs):
        premier_pas = [truc * i if i else None for i in index]

    Je te laisse le boulot pour la logique d’accumulation.

  • kontre

    il ne doit pas y avoir besoin de multiplier truc par i, [truc if i else None for i in index] doit suffire.

  • furankun

    Salut encore!
    voilà une solution pour obtenir ce que je voulais

    mes_index = [[1, 1, 0, 0, 0, 1], [0, 0, 1, 0, 1, 0], [0, 0, 0, 1, 0, 1],  [0, 0, 0, 1, 0, 1], [0, 0, 0, 1, 0, 1],[0, 0, 0, 1, 0, 1],]
    mes_trucs = ['Saucisse','Canard','Gomme','Truc4','Touillette','Bas_resille']
    premier_pas = [None for i in range(len(mes_trucs))]
    for index,truc in zip(mes_index, mes_trucs):
        second_pas =[truc if index[x] else premier_pas[x] for x in range(len(index))]
        premier_pas = second_pas

    Ca remplit la liste résultat second_pas avec truc ou ce qu’il y a dans la liste précédente premier_pas (et ça écrase si besoin).

    Merci beaucoup pour votre aide Sam et kontre (ça marche oui, d’ailleurs), grâce à vous je suis un peu moins con :)

  • Policier Moustachu

    il faudrait corriger:

    lignes_qui_ne_finissent_pas_par_un_point = (l for l in lignes_qui_ne_finissent_pas_par_un_point if not l.endswith(‘.’))

    en

    lignes_qui_ne_finissent_pas_par_un_point = (l for l in lignes_qui_contienne_un_mot if not l.endswith(‘.’))

  • DiZ

    On n’est pas sur stackoverflow mais je n’ai pas pu m’empêcher !

    @furankun:
    Quelques petits trucs pour améliorer encore ton code ;)

    Déjà, tu peux te passer de ta liste second_pas en mettant directement le résultat dans ta liste premier_pas (que j’ai appelé acc ici).

    acc = [elems[i] if mask[k] else v for k,v in enumerate(acc)]

    Petite note sur enumerate(list) au passage : ça te renvoie chaque paire (index, valeur) de ta liste. Tu peux même changer la valeur de départ avec enumerate(list, valeur_depart. Très pratique !

    Ensuite, pour initialiser ta liste, Python dispose d’une syntaxe plus efficace et plus simple également: [un_element] * nombre

    acc = [None] * len(elems)

    Enfin, ici en termes de performance, mieux vaut ne pas modifier un élément de ta liste si celui-ci reste identique. Alors il vaut mieux éviter d’utiliser une ‘comprehension list’ ici.

    Ce qui donne au final :

    elems = ['Saucisse','Canard','Gomme','Truc4','Touillette','Bas_resille']
    masks = [[1, 1, 0, 0, 0, 1],
             [0, 0, 1, 0, 1, 0],
             [0, 0, 0, 1, 0, 1],
             [0, 0, 0, 1, 0, 1],
             [0, 0, 0, 1, 0, 1],
             [0, 0, 0, 1, 0, 1]]
     
    acc = [None] * len(elems)
    for elem,mask in zip(elems,masks):
        for i,idx in enumerate(mask):
            if idx: acc[i] = elem
        print(acc)

    Et si tu veux vraiment utiliser une ‘comprehension list’ :

    acc = [None] * len(elems)
    for elem,mask in zip(elems,masks):
        acc = [elem if idx else acc[i] for i,idx in enumerate(mask)]
        print(acc)

    Voilà voilà !

  • furankun

    Ah la vache, j’avais pas vu! merci DiZ, j’ai bien mal à la tête maintenant :D

  • AnFer

    Bonjour à vous, ça fait un petit moment que je parcours votre site et je le trouve vraiment génial ^^

    j’ai beaucoup (re)appris sur le python grâce à vous ^^

    MAIS, ça fait un moment que je connais mon alphabet (A B B C F D H G… je sais plus la suite) et je crois que contrairement à ce qui est avancé juste au dessus (ordonnées par ordre alphabétique, et numérotées:

    Ce code ne range pas les lignes par ordre alphabétique ^^’

    (je n’ai pas essayé de mon côté pour vérifier mais je ne vois rien dans le code qui rangerais dans l’ordre ^^’)

    >>> 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

    Cordialement AnFer ^^

  • Sam Post author

    Non en effet, c’est le résidu d’une phrase qui correspond à une ancienne version du code avec sorted() qu’on avait viré l’année dernière, visiblement sans mettre à jour le texte. Je vais corriger ça.

  • Anne Onyme

    Plop!

    Je vous propose de profiter de ce dépoussiérage d’été pour corriger les dernières fautes de frappe/d’orthographe/de grammaire, comme ça l’article sera encore plus parfait que parfait.

    J’ai seulement vu: “sets en intensions” -> “sets en intension” (sans ‘s’).

    Si mon commentaire vous fait chier, merci de l’envoyer dans /dev/nul.

  • ZuluPro

    Je vous propose un bonus 3 (qui devrait plutôt être un article complet) avec les listes en intension “combinées”, par exemple:

    colors = ['red', 'green', 'yellow']

    forms = ['circle', 'square', 'triangle']

    [{'color': c, 'form': f} for c in colors for f in forms]

    [{'color': 'red', 'form': 'circle'},

    {'color': 'red', 'form': 'square'},

    {'color': 'red', 'form': 'triangle'},

    {'color': 'green', 'form': 'circle'},

    {'color': 'green', 'form': 'square'},

    {'color': 'green', 'form': 'triangle'},

    {'color': 'yellow', 'form': 'circle'},

    {'color': 'yellow', 'form': 'square'},

    {'color': 'yellow', 'form': 'triangle'}]

    Ca serait vraiment cool un article de votre part là dessus!

    Quelques exemples by myself ici: https://anthony-monthe.me/weblog/2015/08/13/factory-boy-poor/ (en 2e partie)

    N’hésitez pas à me dire si j’ai écris des grosses con*****

  • klerk

    “Ça parait verbeux parce que les noms sont noms, mais en vérité c’est plutôt court:” -> les noms sont longs.

  • Sam Post author

    @ZuluPro: il y a la section “nested comprehension list” qui parle justement de ça. J’y rajoute ton exemple.

    @klerk: merci.

  • Samos

    J’arrive presque 6 ans après la guerre, cependant l’article reste intéressant.

    Petite typo dans le bonus 2 : “Ainsi, sous avez ceci” => “Ainsi, vous avez ceci”
    Dans l’exemple du début, je ne suis pas sûr que la phrase 3 “Con nichon ahhhhhhhh. Ca veut dire bonjour en japonais” contienne le mot “ni”, mais peut-être que je me fourvoie

    Merci

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.