Guido Van Rossum, notre-Dieu-à-tous-beni-soit-il, a un jour accepté de faire une session de questions-réponses publique, dans laquelle un petit malin lui a demandé “Comment ordonner un million d’entiers 32 bits dans 2Mo de Ram avec Python“.
Ca aurait été sur ce blog, le mec se serait pris un tampon “drozophiliafucker” dans la gueule et ça aurait été plié. Mais quand on est BDFL et, à l’époque, employé chez Google, on est obligé de donner une réponse un peu sérieuse.
Si vous avez suivi le lien précédent, vous verrez que sa réponse implique un obscure module appelé heapq. Si vous allez sur la doc du-dit module, vous verrez qu’elle implique une obscure explication innomable et vous aller retourner à la vidéo de porn que vous avez laissé bufferiser en haute résolution afin de pouvoir voir les grains de beauté qui longent le pourtour anal de la principale protagoniste. Respirons.
Il y a une explication rationnelle à tout cela
heapq est un algorithme qui organise une liste sous forme d’arbre binaire. Vous voyez c’était simple non ?
Nan je déconne, je vais pas vous laisser avec ça quand même.
Le module heapq permet d’utiliser le type “list” pour y ajouter ou retirer les éléments de telle sorte qu’ils soient toujours dans l’ordre.
Sous le capot, ça marche effectivement avec un arbre binaire, mais on s’en branle. Tout ce qu’il faut comprendre c’est:
>>> from heapq import heappush, heappop >>> l = [] >>> heappush(l, 69) >>> heappush(l, 42) >>> heappush(l, 2048) >>> heappush(l, -273.15) >>> l # la liste est ordonnée en arbre binaire... [-273.15, 42, 2048, 69] >>> for x in xrange(len(l)): # et on depop pour itérer dans l'odre print heappop(l) ... -273.15 42 69 2048 |
Donc on a une liste, on lui met des éléments dans la tronche dans n’importe quel ordre, et bam, on peut itérer dans le bon ordre sans avoir à rien faire. Et cette insertion est assez rapide (O(lg n) pour les tatillons). Le parcours l’est également (de l’ordre de O(n log n)).
Et donc c’est très pratique pour trouver les x plus petits éléments (ou les plus grands), implémenter des queues de priorité, etc.
Exemple de queue de priorité, (courageusement volé d’ici):
import heapq class PriorityQueue(list): def __init__(self, data): super(Heap, self).__init__() for i, x in enumerate(data): self.push(i, x) def push(self, priority, item): """ On push en rajoute une priorité """ heapq.heappush(self, (priority, item)) def pop(self): """ On pop en retirant la proprité """ return heapq.heappop(self)[1] def __len__(self): return len(self) def __iter__(self): """ Comme on a une méthode next(), on peut se retourner soi-même. Ainsi la boucle for appelera next() automatiquement. """ return self def next(self): """ On depop la liste du plus petit au plus grand. """ try: return self.pop() except IndexError: raise StopIteration >>> l = PriorityQueue(("azerty")) >>> l >>> l.push(100, 'après') >>> l.push(-1, 'avant') >>> l.push(5, 'pendant') >>> for x in l: ... print x ... avant a z e r t pendant y après |
Et le plus beau, c’est qu’on peut prendre plusieurs itérables ordonnés, et utiliser heapq.merge
pour obtenir un générateur (qui ne charge pas tout en mémoire d’un coup) qui va permettre d’iterer de manière ordonnée sur tous les éléments.
>>> import heapq >>> l1 = sorted([random.randint(0, 1000) for x in xrange(5)]) >>> l2 = sorted([random.randint(0, 1000) for x in xrange(5)]) >>> l3 = sorted([random.randint(0, 1000) for x in xrange(5)]) >>> list(heapq.merge(l1, l2, l3)) [52, 59, 60, 171, 174, 262, 336, 402, 435, 487, 557, 645, 899, 949, 996] |
Notez que ce n’est pas du tout la même chose que de concaténer les listes:
>>> l1 + l2 + l3 [59, 174, 336, 487, 996, 52, 171, 557, 645, 949, 60, 262, 402, 435, 899] |
Car des élements de l2
peuvent être inférieurs à ceux de l1
. heap.merge
nous ordonne tout bien correctement. C’est l’équivalent de sorted(l1 + l2 + l3)
, sauf que ça ne charge pas tout en mémoire:
>>> heapq.merge(l1, l2, l3) <generator object merge at 0x0314F238> |
Alors que sorted()
, charge tout en mémoire:
>>> sorted(l1 + l2 + l3) [52, 59, 60, 171, 174, 262, 336, 402, 435, 487, 557, 645, 899, 949, 996] |
Bien entendu, ça marche avec des listes créées avec
heapq
, ainsi:
>>> l1 = []
>>> for x in xrange(5): heappush(l1, random.randint(0, 1000))
>>> l2 = []
>>> for x in xrange(5): heappush(l2, random.randint(0, 1000))
>>> l3 = []
>>> for x in xrange(5): heappush(l3, random.randint(0, 1000))
>>> list(heapq.merge(l1, l2, l3))
[31, 40, 133, 360, 504, 508, 513, 679, 645, 792, 838, 413, 765, 886, 924]
(Grosse connasserie de ma part, faites comme si vous aviez rien vu.)
Quand on a de grosses quantités de données à trier, c’est très pratique, car l’effort de tri est répartie à chaque insertion, et à chaque itération pendant le parcours, pas concentré sur de gros appels de sorted()
.
On peut aussi récupérer des trucs du genre, les n plus petits / grands éléments sans tout coder à la main:
>>> l = [random.randint(0, 1000) for x in xrange(100)] >>> heapq.nsmallest(4, l) [0, 2, 4, 7] >>> heapq.nlargest(3, l) [999, 996, 983] |
Et c’est beaucoup plus efficace que de le faire soi-même.
J’en profite pour rappeler au passage que tous les objets en Python sont ordonnables:
>>> (1, 2) > (2, 1) False >>> (1, 2) < (2, 1) True >>> "a" > "b" False >>> "a" < "b" True |
Et qu’en définissant les méthodes __eq__, __lt__
, __gt__
, etc., on peut donner un moyen de comparer n’importe quelle classe.
Bref, pour tous les besoins de classement, de priorisations, d’ordonnancement, de notion de plus petits, de plus grands, etc. qui concernent un gros jeu de données, heapq
est le module qu’il vous faut.
Et là je percute que ça fait quelque temps déjà que je fais des articles élitistes pour des uses cases de 1% de la population. Donc les prochains tutos concerneront des trucs plus terre à terre. Parce que, je fais le malin là, mais je savais pas à quoi servait heapq
il y a 3 semaines.
xoxo les filles
Tu as fait une faute à “algorithme”. Tu seras fouetté.
Mais sinon, les arbres binaires, c’est intéressant, comme structure (il est surtout très conseillé d’utiliser les structures adaptées à ce qu’on fait ; donc en connaître plusieurs est bien). Moi j’ai jamais été foutu de coder les fonctions de ré-équilibrage d’arbre binaire (en OCaml %]).
J’ai relu mon commentaire précédent, et j’ai repensé à cet article tu m’en veux pas hein ^^ ?
:s/Google/Dropbox/
qui a dit “drozophiliafucker” ?
@Baronsed: il manque l’url dans ton lien ^
@gregr: d’où le “à l’époque”
Très bonne astuce que je ne connaissais pas…Sur de gros volumes de données ça paraît très efficace.
Uh, c’est trié ça [-273.15, 42, 2048, 69] ? 2048 <= 69 ?
J'avais lu la discussion en question sans rien y comprendre (j'avais pas trop cherché, aussi), c'est sympa d'avoir une explication claire !
Bien vu @kontre, j’ai merdé dans ma première explication ! La liste n’est pas ordonnée normalement, mais ordonnées en arbre binaire, il faut donc la poper pour obtenir l’odre correcte. Je vais corriger ça.
À noter, pour ceux que ça amuse, que
heapq
fournit donc les briques de base pour faire un mergesort (suffit de faireheapq.merge
en partant de chaque élément pris séparément) et un heapsort (push
tout,pop
tout) qui ont une meilleure complexité* que le célèbre quicksort.Ceci dit, timsort (qui est derrière sorted) est encore meilleur (http://www.drmaciver.com/2010/01/understanding-timsort-1adaptive-mergesort/ le meilleur article du monde sur comment optimiser un algo de tri).
*: dans le cas le pire. En moyenne ils se valent tous.
C’est pas plutot __cmp__ la méthode qu’il faut implémenter pour qu’une classe quelconque soit comparable ?
En fait c’est __eq__, __gt__, __lt__, etc. __cmp__ est déprécié. Je vais rajouter une précision.
Merci pour l’explication sur ce module méconnu.
Au passage, il manque un l’import de heappop dans le premier exemple :)
C’était pour voir si tu suivais.
Au passage, pour transformer la liste initiale en heap, il est préférable d’utiliser la function heap.heapify plutôt que d’appeler n fois heap.push: O(n) vs. O(n log n).
Commentaire sans rapport avec le sujet, mais le préambule, bouleversant de vécu, est vraiment très drôle